Comments 29
Полезно знать и держать в закладках!
Но, помните, преждевременная оптимизация — зло
Когда-то это называлось «дырявой абстракцией», а предлагаемые лайфхаки — «преждевременной оптимизацией». Также вы забыли упомянуть, что сам Number является по спецификации числом с плавающей точкой двойной точности, и для double-ов деление на 15000 ничем принципиально не отличается от деления на степень двойки, так же как и не определены битовые операции.

Но так как VM использует внутренние оптимизации представления вида «считать беззнаковым 32-битным целым, пока не будет доказано обратное», это сработает. Так что тут «кротовьи норы» второго порядка.

Стоит ли их использовать? Я бы сказал, что нет. Код должен быть читабельным и понятным другому инженеру, который ничего не знает о вашем продукте. И только если результат читабельного кода медленный, и только если профилировщик покажет, что узкое место — конкретно в операции остатка от деления, то только тогда следует придумывать оптимизации.
если профилировщик покажет

О каком таком профилировщике для js, который укажет именно на операцию остатка от деления, вы говорите? Я использую perf, и он ничего подобного не показывает. Вот сравните 2 результата профайлера, в каком из них вы видите просадки от операции остатка от деления?


до оптимизаций
98,60%  node     perf-6945.map        [.] LazyCompile:* /home/blah/dev/before.js:1
 0,05%  node     node                 [.] _ZN2v88internal7Scanner4ScanEv
 0,05%  node     node                 [.] _ZN2v88internal7Scanner28ScanIdentifierOrKeywordInnerEPNS1_12LiteralScopeE
 0,03%  node     node                 [.] _ZN2v88internal6String17CalculateLineEndsENS0_6HandleIS1_EEb
 0,03%  node     perf-6945.map        [.] Handler:UnknownBytecodeHadler

после оптимизаций
95,09%  node     perf-6827.map        [.] LazyCompile:* /home/blah/dev/after.js:1
0,16%   node     node                 [.] _ZN2v88internal7Scanner28ScanIdentifierOrKeywordInnerEPNS1_12LiteralScopeE
0,13%   node     node                 [.] _ZN2v88internal7Scanner4ScanEv
0,08%   node     node                 [.] _ZN2v88internal7Scanner4NextEv
0,07%   node     node                 [.] _ZN2v88internal7Scanner7AdvanceILb0ELb1EEEvv
0,07%   node     node                 [.] _ZN2v88internal7Scanner21SkipSingleLineCommentEv
0,07%   node     node                 [.] _ZN2v88internal6String17CalculateLineEndsENS0_6HandleIS1_EEb
0,06%   node     node                 [.] _ZN2v88internalL24KeywordOrIdentifierTokenEPKhi.part.0

Суровая реальность в том, что не всегда можно полагаться на профайлер. А если вы захотите его расчехлить, будьте готовы расшифровывать его инструкции. Именно поэтому нужно знать узкие места своего языка, знать когда и как он работает. Но читаемость кода никогда не должна страдать, это точно.

Код должен быть читабельным и понятным другому инженеру, который ничего не знает о вашем продукте.

Но знает язык на должном уровне, согласны? И, очевидно, примерно помнит, чему равны степени двойки.
Я к тому, что приведённые примеры — не ухудшают читаемости кода. Для программиста число 65536 должно быть не более «магическим», чем 1000. А что память быстрее всего копировать блоками, равными разрядности шины — это общеизвестный факт.
Никакого занудства, всё правильно. 2^16=65536, поэтому два байта кодируют с 0 по 65535.

Про преждевременные оптимизации и читабельность твердят на каждом шагу. Как будто это что-то плохое. В результате имеем тонны тормозных решений просто потому, что инженеры с умным видом сцат писать иначе.

Матиас очень любит делать хардкорные вещи. Одна из последних его вещей — json парсер на чистом js, который кратно быстрее стандартного. Если вы тоже любите оптимизации, то подписаться на его гитхаб однозначно.


https://github.com/mafintosh

это интересные знания, если хочется получше разобраться, как работает движок. но «не пытайтесь повторить это дома». даже если это и даёт прирост в скорости (что само по себе не известно, нужно ли вашему приложению) — поддерживаемости коду это точно навредит
Зачем вводить новый термин «кротовая нора», когда на самом деле это просто оптимизация? Чтобы звучало модно, стильно, молодежно?
Видимо, какой-то англоязычный жаргонизм, калькированный переводчиком.

Wormholes же в оригинале. И картинка намекает, а перевод не очень удачный. Wormholes в данном случае типа залез в чёрную дыру тут, а вылез в 100 млн парсеков (за 1000 лет до рождения). Червоточины, возможно, было бы точнее и понятнее

Это не претензия к переводчику, а вопрос по существу. Что из описанного в статье нельзя было считать обычной оптимизацией?

Все являются, конечно. Главная идея статьи (поста в блоге в оригинале) в последней фразе. Перефразируя "JS такой высокоуровневый, а битовые хаки для оптимизации все равно работают".

Почти всё. Тот факт, что операции с «нетипизированными данными», которые на самом деле «Number», которые типа как «double», но на самом деле «uint32» и оптимизатор про это знает — очень серьёзно опирается на реализацию многих вещей.

То есть фактически подобные вещи «пробивают» все мнгочисленные слои абстракций, которые пытаются сделать вид, что JS — это такая себе сущность, описанная в спецификации… и никакого отношения к ассемблеру не имеет… а оказывается, что нет — таки имеет…

И что характерно, на самом деле многие скриптовые/высокоуровневые языки такими "битовыми" хаками особо не пооптимизируешь. Вот из того, что помню


  • SQL — за современный Postgres с Jit не скажу, но в MS SQL Server/MySQL/Oracle/PostgreSQL разница на уровне погрешности.
  • VB/VBS/VBA. Некоторые приемы работают, но только при строгой типизации. Variant или работа с COM мгновенно съедают разницу.
  • 1С — там арифметика на своём "особенном" числовом типе. Этот тип раз в 100 медленнее Int32. Сдвигов и битовой логики нет.
  • Powershell — с ним смешно. Я лет 6 назад с удивлением обнаружил, что типизированные переменные медленнее нетипизированных. Там просто при каждом присваивании для типизированных переменных происходит принудительное приведение к типу.
  • Bash, cmd — не найти разницу

Да и JS стал таким чувствительным к битовым хакам примерно с появлением "дерзкого" V8, потом остальные стараются догнать (кто не верит — ставьте виртуалку с IE6 и проверьте).

Если вы привыкли видеть сквозь синтаксис Javascript макроассемблер, то конечно ни чего необычного. Просто обычно вопросы оптимизации замыкаются на языка и библиотек (for-of vs .forEach(), DOM vs jQuery и т.п.). smile)

Умные компиляторы или VМ способны производить такую оптимизацию превращая за кулисами операцию получения остатка в побитовую операцию и обратно. По факту последняя V8 Javascript VM (не реализовано в NodeJS) делает именно так.

NodeJS 11 вышла и там апдейт V8 до последней версии, может уже и реализовано.

Пока не заглянул в MDN, не смог понять, что Math.clz32 возвращает количество лидирующих нулевых битов в 32-битном двоичном представлении числа.

Функция copy (та, которая использует Float64Array) иногда копирует данные неправильно. Даже если длина массива делится на 8. Это хороший пример того, как "умная" оптимизация может приводить к появлению трудно обнаруживаемых ошибок.

Под спойлером — пример массива, который копируется неправильно. Но я предлагаю перед тем, как заглядывать туда, попробовать найти проблему самостоятельно.


Код
let input = new Uint8Array(8).fill(255);
let output = new Uint8Array(8);
copy(input, output);
console.log(input, output);

Объяснение

При декодировании данных как Float64, получается значение NaN. Это единственное значение, у которого есть несколько различный представлений (хотя значения +0 и -0 выглядят как равные для операторов == и ===, они являются различными). Согласно стандарту, в JavaScript все значения NaN неразличимы:


The Number type has exactly 18437736874454810627 (that is, 264−253+3) values, representing the double-precision 64-bit format IEEE 754-2008 values as specified in the IEEE Standard for Binary Floating-Point Arithmetic, except that the 9007199254740990 (that is, 253−2) distinct “Not-a-Number” values of the IEEE Standard are represented in ECMAScript as a single special NaN value. (Note that the NaN value is produced by the program expression NaN.) In some implementations, external code might be able to detect a difference between various Not-a-Number values, but such behaviour is implementation-dependent; to ECMAScript code, all NaN values are indistinguishable from each other.

При кодировании в байты, скорее всего, будет использоваться не то представление NaN, которое было в исходном массиве (стандарт не указывает, какое именно нужно использовать, так что результат зависит от реализации), поэтому скопированные данные не будут совпадать с исходными.

Спасибо!
Нутром чуял, что нельзя байтовый массив безболезненно в массив Float-ов сконвертить… Не знал почему, но со времён школы и QBasic-а натыкался на порчу данных при этом процессе.
В js появляется BigInt64Array (пока спецификация не введена, поэтому не во всех браузерах, и скорость не мерял; ну и конечно же предложенный TypedArray.prototype.set лучше).
несколько лет назад (еще до появления asm.js) сравнивал скорость копирования, и на некоторых браузерах UInt32Array был быстрее, чем Float64Array.

wibuhu Добавили бы ремарку рядом с этим примером, что его нельзя использовать.

Only those users with full accounts are able to leave comments. Log in, please.