文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

turbo.js 是一个很小的 JavaScript 库,用来简化并行的复杂计算。其设计的计算(内核执行)是基于 GPU 进行的,这使得其可以在同一时间对一组数据进行同时操作。

turbo.js 兼容所有的浏览器(甚至是不使用 ES6 模板字符串的 IE)以及几乎所有桌面和移动的 GPU。

在线演示和简单的介绍请看 turbo.github.io.

示例1

在上述网站我们可以看到对一个非常大的数组值进行简单计算的例子。

第 1 段(可获 1.33 积分)

turbo.js 只包含两个函数供你调用,两个函数都存在于 turbojs 对象中。如果该对象没有初始化,就会发生错误。因此我们在使用它之前必须先检查是否支持 turbo.js。你也可以选择是否对 turbo.js 抛出的异常进行捕获,异常信息会包含更详细的错误详情。

if (turbojs) {
  // yay
}

现在我们需要一些内存,因为数据需要在 GPU 和系统内存之间进行传输,我们需要减少这个传输操作导致的资源开销。为了实现这个,turbo.js 提供了 alloc 函数,它将同时在 GPU 和浏览器中预留内存。JavaScript 可以访问并修改预留内存中的内容,只需要访问包含所分配内存的变量的 .data 子数组即可。

第 2 段(可获 1.55 积分)

不管是 turbo.js 还是 JavaScript,分配内存的类型是严格的,并通过 32 位 IEEE 浮点数值的一维数组呈现。所以 .data 子数组是一个标准的 JavaScript Float32Array 对象。内存分配完毕后,你可以对该数组进行任意操作,但不能更改其大小。如果你试图修改数组大小,将会遇到未知错误。

if (turbojs) {
  var foo = turbojs.alloc(1e6);
}

我们现在有了一个包含 100 万个元素的数组,下面给数组填充一些值。

if (turbojs) {
  var foo = turbojs.alloc(1e6);

  for (var i = 0; i < 1e6; i++) foo.data[i] = i;

  // print first five elements
  console.log(foo.data.subarray(0, 5));
}
第 3 段(可获 0.89 积分)

运行上述代码,控制台将显示 [0, 1, 2, 3, 4]。现在来做一些简单运算:每个值乘以 nFactor 并打印结果:

if (turbojs) {
  var foo = turbojs.alloc(1e6);
  var nFactor = 4;

  for (var i = 0; i < 1e6; i++) foo.data[i] = i;

  turbojs.run(foo, `void main(void) {
    commit(read() * ${nFactor}.);
  }`);

  console.log(foo.data.subarray(0, 5));
}

控制台现在显示的是 [0, 4, 8, 12, 16]. 是不是很简单?让我们来分析一下前面做了什么:

  • turbojs.run 的第一个参数是之前我们分配好的内存,第二个参数是对数组值所要执行的代码
  • 代码使用名为 GLSL 编写,这是一个 C 的扩展语言。如果你对这个语言不熟悉,可以搜索一下看看。如果你熟悉 C 语言就可以很快上手 GLSL。
  • 核心代码只包含一个 main 函数,无需任何参数。不过可以包含一个或者多个函数。
  • read() 函数读取当前输入值
  • ${nFactor} 相当于 nFactor 的值,因为 GLSL要求数字常亮表达式必须是有类型的,因此我们追加了一个 . 来标识为浮点类型,否则 GLSL 编译器将会抛出类型错误的信息。
  • commit() 将计算结果写会内存,你可以在任意函数中 commit,但一个良好的实践是在 main 函数的最后一行进行 commit 操作。
第 4 段(可获 2.48 积分)

示例2: 向量

有些时候你希望在一个操作中返回多个值,尽管看起来不太好,但我们一直都这样做。commit 和 read 实际上都支持 4 维的向量:

  • vec4 read() 返回 GLSL 的数据类型 vec4.
  • void commit(vec4) 要求 vec4 参数并写到内存

vec4 类型其实就是一个数组,你可以认为是相当于 JavaScript 这样的形式:foobar = {r:0, g:0, b:0, a:0} ,与 JavaScript SIMD  的 Float32x4 非常相似。

GLSL 很棒的一点是所有操作都是可重载的,因此它可以处理向量而不是单独处理每个元素,因此

第 5 段(可获 1.4 积分)
commit(vec4(read().r * 4., read().g * 4., read().b * 4., read().a * 4.));

就等同于

commit(read() * 4.);

是不是简洁很多?当然了,在 GLSL 中还有其他类型的向量,名为 vec2vec3。如果你创建一个更大的向量,并提供一个小一点的作为参数,GLSL 将自动对向量的值进行对齐:

vec2 foo = vec2(1., 2.);

commit(vec4(foo.r, foo.g, 0., 0.));

// 等同于

commit(vec4(foo.rg, 0., 0.));

现在我们将要使用这些了。如果你访问上述提到的网站,你会看到一个关于使用 JS 和 JS+turbo.js 的性能比较。在那个比较中,我们在曼德尔布罗特分形函数中计算随机点。让我们从 JavaScript 代码开始来分解测试的过程:

第 6 段(可获 1.08 积分)

每一次运行,每个 vec4 内存分配的前两个值使用随机坐标进行填充,并作为分形函数的输入:

for (var i = 0; i < sampleSize; i += 4) {
  testData.data[i] = Math.random();
  testData.data[i + 1] = Math.random();
}

每一个操作结果将会产生一个灰度色值,这个数值将被写到每一个向量的第三个元素:

function testJS() {
    for (var i = 0; i < sampleSize; i += 4) {
        var x0 = -2.5 + (3.5 * testData.data[i]);
        var y0 = testData.data[i + 1], x = 0, y = 0, xt = 0, c = 0;

        for (var n = 0; n < sampleIterations; n++) {
            if (x * x + y * y >= 2 * 2) break;

            xt = x * x - y * y + x0;
            y = 2 * x * y + y0;
            x = xt;
            c++;
        }

        var col = c / sampleIterations;

        testData.data[i + 2] = col;
    }
}
第 7 段(可获 0.61 积分)

上述代码用来计算 sampleIterations 的迭代深度。然后我们再来看看在 turbo.js 中怎么计算相同的任务:

function testTurbo() {
    turbojs.run(testData, `void main(void) {
        vec4 ipt = read();

        float x0 = -2.5 + (3.5 * ipt.r);
        float y0 = ipt.g, x, y, xt, c;

        for(int i = 0; i < ${sampleIterations}; i++) {
            if (x * x + y * y >= 2. * 2.) break;

            xt = x * x - y * y + x0;
            y = 2. * x * y + y0;
            x = xt;
            c++;
        }

        float col = c / ${sampleIterations}.;

        commit(vec4(ipt.rg, col, 0.));
    }`);
}
第 8 段(可获 0.3 积分)

看看 JavaScript 代码转为 GLSL 是有多简单,没有特别的方式。当然了,这个例子并不是在 JavaScript 和 GLSL 中最优化的算法,我们只不过是为了比较而已。

示例3: 调试

GLSL 代码是通过 GPU 向量编译器进行编译的。通常这些编译器都会提供非常详尽的错误信息。你可以通过捕获 turbo.js 抛出的异常来获取编译时的错误。例如下面代码所示:

if (turbojs) {
  var foo = turbojs.alloc(1e6);
  var nFactor = 4;

  turbojs.run(foo, `void main(void) {
    commit(${nFactor}. + bar);
  }`);
}
第 9 段(可获 0.99 积分)

上面代码会产生两个错误,第一个是 bar 未被初始化;第二个是类型不匹配:commit 函数的参数要求是向量,但却传递了浮点类型。打开你的浏览器控制台就会看到项目错误信息:

注意事项

  • 你最好提供一个 JavaScript 回退方法,用于浏览器不支持 turbo.js 的情况
  • 使用 web workers 来处理大数据集,避免页面操作堵塞
  • 使用假数据来对 GPU 进行热身,否则将无法得到 GPU 的充分性能表现
  • 关于错误检查,使用小的数据集进行检查,如果检查不通过则回退到 JavaScript 方法
  • 我没有试过,但我猜你会用 glsl-transpiler 来自动创建 JS 回退代码
  • 认真考虑你是否真的需要用 turbo.js ,你应该优先考虑算法的优化,考虑使用 JS SIMD。turbo.js 并不适用于非并行计算场景。
第 10 段(可获 1.95 积分)

文章评论

中山访客
浏览器越来越牛