SHA-256 без единой битовой операции стоит 925 195 шагов интерпретатора на блок, и это уложилось в предел только после трёх названных правок
Битовых операций в языке нет вовсе: среди восемнадцати встроенных форм (flang/src/builtins.mjs) нет ни «и», ни «или», ни «исключающего или», ни сдвига. Хеш пришлось считать арифметикой: бит k числа — это ((х остаток от 2^(k+1)) минус (х остаток от 2^k)) делить на 2^k, а исключающее или БИТА — (бит первого плюс бит второго) остаток от 2.
Главное решение — считать ПОРАЗРЯДНО, а не побитово. Наивный перевод формул FIPS 180-4 дал бы по свёртке на каждую пару операндов, то есть восемь свёрток в раунде. Их четыре, и разницу дали три наблюдения:
Σ0иΣ1— это исключающее или ТРЁХ значений, а не двух подряд. Сумма трёх битов по модулю 2 считается одной свёрткой;Ch(e, f, g)— поразрядный ВЫБОР: где бит первого единица, берётся бит второго, иначе третьего. Запись стандарта(e и f) исключающее или (не e и g)считает то же самое втрое дороже;Maj(a, b, c)— поразрядное БОЛЬШИНСТВО: единица там, где хотя бы двое из трёх. Снова одна свёртка вместо трёх операций.
Ещё одна мелочь, которая заметно дешевле очевидной записи: вклад разряда можно не собирать умножением. (х остаток от 2вес) минус (х остаток от вес) равно либо нулю, либо самому весу — то есть это УЖЕ вклад, и делить на вес, чтобы потом умножить обратно, незачем. Отсюда «исключающее или трёх» на разряде — это сумма трёх вкладов остаток от (вес умножить на 2), а «большинство» — (сумма минус (сумма остаток от (вес умножить на 2))) делить на 2. Ни одного условия, ни одного деления на вес.
Чем подтверждено, числом. Двоичный поиск по flang run --max-steps на входе [97, 98, 99] (одно сообщение = один блок), ветка work/bayty:
| редакция | шагов на блок |
|---|---|
| «Бит» отдельной функцией, три вызова на виток | 1 394 531 |
| извлечение бита внесено в тело свёртки | 983 593 |
| плюс снятые лишние сведения к слову и сторож на степень двойки | 925 195 |
Предел интерпретатора по умолчанию — 1 000 000 шагов, и у flang check ключа --max-steps нет. То есть первая редакция официальный вектор примером нести не могла, а третья несёт: «Хеш байтов» от пустого сообщения и от abc стоят примерами и гоняются при каждой проверке.
Два пути измерены и отвергнуты.
- Повторять подвыражение вместо
пустьвнутри свёртки — дороже. Замена двухпустьна трижды повторённое(вес плюс вес)дала 1 057 226 шагов против 983 593. То есть связывание имени в теле свёртки стоит заметно меньше, чем лишнее вычисление, и «убрать переменную ради скорости» здесь неверно. - Обрабатывать по два разряда за виток — почти ничего не даёт. Свёртка по 16 весам с двумя разрядами на виток: 963 281 против 983 593, то есть 2 %. Значит цена свёртки почти вся в ЧИСЛЕ УЗЛОВ тела, а не в накладных расходах витка, и укрупнять шаг бессмысленно — надо сокращать выражение.
Чем ограничено. Замер сделан на интерпретаторе. Напечатанный в JavaScript модуль считает миллион байт за 24 секунды — это в сотни раз быстрее интерпретатора и всё равно на три порядка медленнее node:crypto. Хеш на flang пишется ради ВОСПРОИЗВОДИМОСТИ формулы двумя сторонами (сторона на C99 у двоичного компилятора), а не ради скорости; там, где нужна скорость, зовут рантайм.
Связано: a-module-address-is-the-sha256-of-its-source, the-interpreter-step-limit-decides-what-can-be-an-example, bottleneck-moved-to-body-shape