Работа с Javascript TypedArray

Почему TypedArrays не быстрее обычных массивов? Я хочу использовать значения precalc для 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];

enter image description here

PS Могут быть мои перфекционные тесты недействительными, не стесняйтесь меня исправлять.

Ответы

Ответ 1

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 и ваши петли выходят за пределы 3 из 4 раз. Но, к сожалению, вы никогда не получите ошибок, только плохие показатели.

Используя новый ArrayBuffer, вы должны объявить Uint32 следующим образом:

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

Смотрите jsperf с (0x10000) и jsperf с (0x10000 * 4).

Ответ 2

Современные двигатели будут использовать истинные массивы за кулисами, даже если вы используете Array, если они думают, что могут, отступая от массивов карт свойств, если вы делаете что-то, что заставляет их думать, что они не могут использовать истинный массив.

Также обратите внимание, что в качестве radsoc указывает, 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 потерял это тест и его результаты, и теперь полностью отключен)

graph showing roughly equivalent performance

Это говорит о том, что, поскольку вы заполняете массив простым и предсказуемым способом, современный движок продолжает использовать истинный массив (с его преимуществами по производительности) под обложками, а не смену. Мы видим в основном такую ​​же производительность для обоих. Разница в скорости может относиться к преобразованию типа, принимающего значение Uint32 и присваивая его sum как number (хотя я удивлен, если преобразование этого типа не отложено...).

Добавьте некоторый хаос:

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 проиграл этот тест и его результаты)

bar graph showing marked performance improvement for typed arrays

Умные, эти инженеры-разработчики 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 потерял это испытание и его результаты) По-видимому, двигатели довольно умны, и сохраните это неэлементное свойство в стороне, продолжая использовать истинный массив для свойств элемента:

bar graph showing performance improvement for standard arrays when <code>Array</code> array gets non-element property

Я немного потерял объяснение, почему стандартные массивы должны быть быстрее там по сравнению с нашим первым тестом выше. Погрешность измерения? Капризы в 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 потерял этот тест и его результаты)

graph showing typed arrays winning except on IE11

Ответ 3

В вашем случае причина плохой производительности - вы пытаетесь читать вне массива при использовании Uint32Array из-за ошибки с длиной массива.

Но если это не будет реальной причиной, то:

Попробуйте использовать Int32Array вместо Uint32Array. Я думаю, что в V8 переменные не могут иметь тип uint32, но могут иметь int32/double/pointer. Поэтому, когда вы назначаете тип uint32 переменной, он будет преобразован в более медленный double.

Если вы используете 32-битную версию V8, тогда переменные могут иметь тип int31/double/pointer. Таким образом, int32 также будет преобразован в double. Но если вы используете обычный массив, а все значения - int31, тогда преобразование не требуется, поэтому обычный массив может быть быстрее.

Также использование int16 может потребовать некоторых преобразований для получения int32 (из-за дополнения знака и одного). Uint16 не требует преобразования, поскольку V8 может просто добавить нули влево.

PS. Вам могут быть интересны указатели и int31 (или int32 на x64) - это те же самые вещи в V8. Это также означает, что int32 потребует 8 байтов на x64. Кроме того, это причина, по которой на x86 нет типа int32: поскольку, если бы мы использовали все 32 бита для хранения целого числа, у нас больше не было бы места для сохранения указателей.