【问题标题】:Javascript TypedArray performanceJavascript TypedArray 性能
【发布时间】:2014-09-11 06:50:06
【问题描述】:

为什么 TypedArrays 不比普通数组快?我想为 CLZ 使用预先计算的值(计算前导零函数)。而且我不希望他们解释为通常的对象?

http://jsperf.com/array-access-speed-2/2

准备代码:

 Benchmark.prototype.setup = function() {
   var buffer = new ArrayBuffer(0x10000);
   var Uint32 = new Uint32Array(buffer);
   var arr = [];
   for(var i = 0; i < 0x10000; ++i) {
     Uint32[i] = (Math.random() * 0x100000000) | 0;
     arr[i] = Uint32[i];
   }
   var sum = 0;
 };

测试 1:

sum = arr[(Math.random() * 0x10000) | 0];

测试 2:

sum = Uint32[(Math.random() * 0x10000) | 0];

PS:可能是我的性能测试无效,请随时纠正我。

【问题讨论】:

  • 您是否删除了 jsperf 测试?我不能再访问它们了
  • 不,我没有删除它。这很奇怪。
  • 非常非常奇怪他们会就这样消失!所有这些……鉴于this FAQ entry,特别奇怪。我已经搜索过 URL 是否发生变化或其他情况,并且......什么都没有。我可以找到其他阵列性能测试,但不是 Sukhanov 和我的。我提出了issue for it。当然,如果我们中的更多人donated to jsPerf 可能不会有什么坏处(我只是这样做了......第一次ducks head)。

标签: javascript performance typed-arrays


【解决方案1】:

在您的情况下,性能不佳的原因是您在使用 Uint32Array 时尝试在数组外部读取,因为数组长度存在错误。

但如果这不是真正的原因,那么:

尝试使用 Int32Array 而不是 Uint32Array。我认为在 V8 中变量不能有 uint32 类型,但可以有 int32 / double / 指针。因此,当您将 uint32 类型分配给变量时,它将被转换为较慢的双精度。

如果您使用 32 位版本的 V8,那么变量可以具有 int31 / double / 指针类型。所以 int32 也会被转换成双精度。但是如果你使用普通数组并且所有值都是 int31 则不需要转换,所以普通数组可以更快。

此外,使用 int16 可能需要进行一些转换才能获得 int32(因为符号和一个的补码)。 Uint16 不需要转换,因为 V8 可以在左侧添加零。

PS。您可能感兴趣,指针和 int31(或 x64 上的 int32)在 V8 中是相同的东西。这也意味着 int32 在 x64 上需要 8 个字节。这也是 x86 上没有 int32 类型的原因:因为如果我们将所有 32 位用于存储整数,我们将没有任何空间来保存指针。

【讨论】:

    【解决方案2】:

    现代引擎将在幕后使用真正的数组,即使您认为可以使用Array,如果您做了一些让他们认为他们不能使用真正的数组的事情,则退回到属性映射“数组”。

    还要注意radsoc points out,var buffer = new ArrayBuffer(0x10000)然后var Uint32 = new Uint32Array(buffer)产生一个Uint32数组,其大小为0x4000(0x10000 / 4),而不是0x10000,因为你给ArrayBuffer的值是以字节为单位的,但当然每个 Uint32Array 条目有四个字节。以下所有内容都使用new Uint32Array(0x10000) 代替(并且一直这样做,甚至在这次编辑之前)来比较苹果和苹果。

    所以让我们从 new Uint32Array(0x10000) 开始:http://jsperf.com/array-access-speed-2/11 (遗憾的是,JSPerf 丢失了这个测试及其结果,现在完全离线) p>

    这表明,由于您以一种简单、可预测的方式填充数组,因此现代引擎会继续在幕后使用真正的数组(具有性能优势),而不是转移。我们看到两者的性能基本相同。速度上的差异可能与采用 Uint32 值并将其作为 number 分配给 sum 的类型转换有关(尽管如果不推迟类型转换,我会感到惊讶......)。

    添加一些混乱,但是:

    var Uint32 = new Uint32Array(0x10000);
    var arr = [];
    for (var i = 0x10000 - 1; i >= 0; --i) {
      Uint32[Math.random() * 0x10000 | 0] = (Math.random() * 0x100000000) | 0;
      arr[Math.random() * 0x10000 | 0] = (Math.random() * 0x100000000) | 0;
    }
    var sum = 0;
    

    ...因此引擎不得不依赖老式的属性映射“数组”,并且您会看到类型化数组的性能明显优于老式类型:http://jsperf.com/array-access-speed-2/3 (遗憾的是,JSPerf 已经失去了这个测试及其结果)

    聪明的,这些 JavaScript 引擎工程师……

    不过,您对 Array 数组的非数组性质所做的具体操作很重要;考虑:

    var Uint32 = new Uint32Array(0x10000);
    var arr = [];
    arr.foo = "bar";                            // <== Non-element property
    for (var i = 0; i < 0x10000; ++i) {
      Uint32[i] = (Math.random() * 0x100000000) | 0;
      arr[i] = (Math.random() * 0x100000000) | 0;
    }
    var sum = 0;
    

    这仍然可以预测地填充数组,但是我们向它添加了一个非元素属性 (foo)。 http://jsperf.com/array-access-speed-2/4 (遗憾的是,JSPerf 已经失去了这个测试及其结果) 显然,引擎非常聪明,并且在继续运行的同时将非元素属性放在一边为元素属性使用真正的数组:

    我有点茫然地解释为什么标准数组应该比我们上面的第一个测试更快。测量误差? Math.random 中的变幻莫测?但我们仍然很确定 Array 中的特定于数组的数据仍然是一个真正的数组。

    如果我们做同样的事情,但填写相反的顺序:

    var Uint32 = new Uint32Array(0x10000);
    var arr = [];
    arr.foo = "bar";                            // <== Non-element property
    for (var i = 0x10000 - 1; i >= 0; --i) {    // <== Reverse order
      Uint32[i] = (Math.random() * 0x100000000) | 0;
      arr[i] = (Math.random() * 0x100000000) | 0;
    }
    var sum = 0;
    

    ...我们回到类型化数组获胜——除了在 IE11 上:http://jsperf.com/array-access-speed-2/9 (遗憾的是,JSPerf 输掉了这个测试及其结果)

    【讨论】:

    • 很有趣,谢谢。在实际用例中事情变得更加复杂,各种对象/数组的连续分配/取消分配会导致内存碎片,如果内存没有预先分配,处理时间会爆炸。在现实生活中,Typed Array 的成本会更低,因为它们是预先分配的(Rq : js Arrays 也可能是预先分配的......)
    • 将预先计算的 int32 值写入类型化数组很快。该数组不包含孔(是的,孔很快但是……)。数组是预先分配的。所以创建和初始化类型数组很快。但是,当您尝试从 Uint32Array 读取该值时,应将其转换为双精度(请参阅下面的答案)。您应该使用 Int32Array(在 x64 上)或 Int16Array(在 x86 上)(再次参见下面的答案)。
    【解决方案3】:
    var buffer = new ArrayBuffer(0x10000);
    var Uint32 = new Uint32Array(buffer);
    

    与以下内容不同:

    var Uint32 = new Uint32Array(0x10000);
    

    不是因为新的 ArrayBuffer(你总是得到一个数组缓冲区:在这两种情况下都看到 Uint32.buffer),而是因为长度参数:使用 ArrayBuffer,每个元素有 1 个字节,使用 Uint32Array,每个元素有 4 个字节。

    因此,在第一种情况下(在您的代码中),Uint32.length = 0x1000/4 并且您的循环 4 次中有 3 次超出范围。但遗憾的是,你永远不会出错,只会出错。

    使用'new ArrayBuffer',你必须像这样声明Uint32:

    var buffer = new ArrayBuffer(0x10000 * 4);
    var Uint32 = new Uint32Array(buffer);
    

    请参阅 jsperf with (0x10000)jsperf with (0x10000 * 4)

    【讨论】:

    • 很遗憾人们会根据内容大小而不是正确性来接受答案。
    • @MorrisLiang,我也是这么想的,但后来我意识到这个答案是在提问半年后才发布的。
    • @MorrisLiang:请注意,我的回答总是使用new Uint32Array(0x10000) 来比较苹果和苹果的长度,但仍然显示出速度差异。但我并没有像上面的 radsoc 那样指出差异。
    猜你喜欢
    • 1970-01-01
    • 2015-09-05
    • 1970-01-01
    • 2014-11-19
    • 2016-06-04
    • 2011-11-20
    • 2016-10-04
    • 2016-01-24
    • 2010-11-02
    相关资源
    最近更新 更多