Разгоняем JS-парсер с помощью WebAssembly (часть 3: SIMD)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В предыдущей статье мы остановились на варианте, который с помощью SWAR-хинта превращает 8 последовательных цифр в одно числовое 32bit-значение. Но что если мы предположим, что все значения у нас, в основном, невелики - до 3 цифр? Тогда нам вполне достаточно использовать всего лишь 32bit-инструкции, а SWAR будет выполнен за 2 операции вместо 3 - сплошной выигрыш!

Давайте перепишем наш код так, чтобы первый блок из 4 символов обрабатывался 32bit-инструкциями, а если понадобится второй блок из 8 символов - уже 64bit-инструкциями.

И... вместо 26ms получаем 28ms! Значит, наше предположение относительно длины чисел не оправдалось, и в первом блоке выгоднее обрабатывать сразу побольше символов.

большие - но по 5, маленькие - но по 3
большие - но по 5, маленькие - но по 3

То есть больше размерность регистра - лучше? И такие регистры есть - это 128-битные SSE-регистры XMM - в WebAssembly они доступны нам как переменные с типом v128 и суперскалярные операции над ними.

Обратите внимание, что пока не все эти операции реализованы в разных JavaScript-движках.

В зависимости от используемой инструкции v128-регистр интерпретируется либо как единый блок (например, для битовых логических операций and, or, xor, not), либо как набор блоков меньшей размерности:

Как включить SIMD в Node.js

Для начала нам придется немного доработать наш "компилятор" compile-test.js: и включить дополнительные расширения:

const wasmModule = wabt.parseWat(
  inputWat
, readFileSync(inputWat, 'utf8')
, {simd : true} // включаем поддержку SIMD-feature
);

Посмотреть, какие существуют другие дополнительные "фичи" компиляции в WASM можно на демо-странице wat2wasm.

Поскольку в Node.js пока поддержка SIMD проходит в качестве экспериментальной, запускать наш парсер придется с включенными соответствующими V8-опциями:

  --experimental-wasm-simd (enable prototype SIMD opcodes for wasm)
        type: bool  default: false
  --wasm-simd-post-mvp (allow experimental SIMD operations for prototyping that are not included in the current proposal)
        type: bool  default: false

Теперь команда на запуск выглядит вот так:

node --experimental-wasm-simd --wasm-simd-post-mvp --expose-gc buffers-test.js

SIMD-парсинг натуральных чисел на WASM

Собственно, нам необходимо решить те же задачи, что и для SWAR-варианта, но уже с учетом возможностей v128-инструкций. За основу возьмем алгоритм, приведенный на StackOverflow.

Узнаем длину числа

  1. вычитаем "нулевой" символ 0x30 из каждого, с переходом через 0 - это означает, что наши стоп-символы 0x0A, 0x20 и 0x2C превратятся в 0xDA, 0xF0 и 0xFC соответственно

  2. вычисляем для каждого байта, что он <= 0x09 - то есть исходно находился в диапазоне 0x30..0x39, что соответствует символам '0'..'9'

  3. находим первый false-байт с помощью ctz(not(bitmask)) - это и есть позиция нашего стоп-символа

Для реализации этого в коде нам понадобятся две глобальные переменные-"константы": "все нули" и "все девятки":

  (global $all_zero v128
    (v128.const i8x16 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30)
  )
  (global $all_nine v128
    (v128.const i8x16 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09)
  )
  ;; разбор числа - SIMD
  (func $parseNumber
    (local $ch i32)
    (local $xword v128)
    (local $xn i32)

    ;; xword = line[off..+15]
    (local.set $xword
      (v128.load align=4
        (global.get $off)
      )
    )

    ;; xn = num digits
    (local.set $xn
      (i32.ctz                         ;;        * = 5
        (i32.xor                       ;; 0b...  1  0  0  0  0  0
          (i8x16.bitmask               ;; 0b...  0  1  1  1  1  1
            (i8x16.le_u                ;; 0x... 00 FF FF FF FF FF
              (i8x16.sub               ;; 0x... F0 05 04 03 02 01
                (local.get $xword)     ;; 0x... 20 35 34 33 32 31
                (global.get $all_zero) ;; 0x... 30 30 30 30 30 30 
              )
              (global.get $all_nine)   ;; 0x... 09 09 09 09 09 09
            )
          )
          (i32.const 0xFFFFFFFF)       ;; 0b...  1  1  1  1  1  1
        )
      )
    )

Определяем стоп-символ

Просто так взять xn-й байт из v128-значения нельзя - нет соответствующей инструкции. Зато есть i8x16.extract_lane, которая позволяет взять заранее указанный байт (нас вполне устроит #0).

Но чтобы в нулевом байте оказался xn-й, нужно "переложить" байты в соответствии с маской с помощью swizzle.

А соответствующую маску, состоящую из единственного повторяющегося значения xn, нам позволит сделать инструкция splat.

    ;; ch = xword[xn]
    (local.set $ch           ;; ch = 0x20
      (i8x16.extract_lane_u  ;; xmm[0]
        0                    ;; константа
        (i8x16.swizzle       ;; 0x... 20 20 20 20 20 20
          (local.get $xword) ;; 0x... 20 35 34 33 32 31
          (i8x16.splat       ;; 0x... 05 05 05 05 05 05
            (local.get $xn)  ;; xn = 5
          )
        )
      )
    )

Нормализуем число

Аналогичным образом, с помощью swizzle "развернем" число, чтобы младший разряд оказался в младшем байте, и сразу зачистим от не-цифр за счет свойства этой операции "занулять" байты, которым соответствуют индексы больше 0x0F.

  (global $shuf_reverse v128
    (v128.const i8x16 0x0F 0x0E 0x0D 0x0C 0x0B 0x0A 0x09 0x08 0x07 0x06 0x05 0x04 0x03 0x02 0x01 0x00)
  )
  (global $all_digit v128
    (v128.const i8x16 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F)
  )
    ;; clean & reverse
    (local.set $xword
      (v128.and                        ;; 0x... 00 01 02 03 04 05
        (i8x16.swizzle                 ;; 0x... 00 31 32 33 34 35
          (local.get $xword)           ;; 0x... 20 35 34 33 32 31
          (i8x16.sub                   ;; 0x... FF 00 01 02 03 04
            (global.get $shuf_reverse) ;; 0x... 0A 0B 0C 0D 0E 0F
            (i8x16.splat               ;; 0x... 0B 0B 0B 0B 0B 0B
              (i32.sub                 ;; = 0x0B
                (i32.const 16)
                (local.get $xn)        ;; xn = 5
              )
            )
          )
        )
        (global.get $all_digit)        ;; 0x... 0F 0F 0F 0F 0F 0F
      )
    )

SIMD-конвертация

Итак, мы получили состояние переменной (читай, регистра), когда в каждом из 16 байтов находятся значения цифр, позиционно соответствующие десятичным разрядам числа, которое мы хотим получить - в младшем байте младший разряд:

P O N M L K J I H G F E D C B A == A * 10^0 + B * 10^1 + C * 10^2 + D * 10^3 + ...

В идеале, для реализации алгоритма нам подошли бы инструкции расширенного умножения типа i16x8.extmul_[low|high]_i8x16_u и расширенного попарного сложения i16x8.extadd_pairwise_i8x16_u.

Первая из этих инструкций умножает нижние/верхние половины пары XMM-регистров, воспринимая их как набор 8bit-чисел на входе, расширяя выход до 16bit-чисел:

0x...   FC   FD   FE   FF
         x    x    x    x
0x...   04   03   02   01
         =    =    =    =
0x... 03F0 02F7 01FC 00FF = i16x8.extmul_low_i8x16_u

Вторая попарно складывает соседние "числа" в регистре:

0x... FC + FD   FE + FF
0x...    01F9      01FD = i16x8.extadd_pairwise_i8x16_u

Поскольку они пока не поддерживаются, нам придется их эмулировать с помощью доступных инструкций.

Итак, сначала мы выделим из нашего регистра с помощью i16x8.widen_low_i8x16_u нижнюю половину, расширив 8bit-числа до 16bit и перемножим с маской степеней 10:

  (global $stepA_pow10 v128
    (v128.const i16x8 1 10 100 1000 1 10 100 1000)
  )
;; ...
    ;; i16x8.extmul_low_i8x16_u - not implemented
    (local.set $xtempL
      (i16x8.mul                  ;; H000 G00  F0   E  D000 C00  B0   A
        (i16x8.widen_low_i8x16_u  ;;  0 H|0 G|0 F|0 E | 0 D|0 C|0 B|0 A
          (local.get $xword)      ;;                ... H G F E D C B A
        )
        (global.get $stepA_pow10) ;; 1000 100  10   1  1000 100  10   1
      )
    )

Степени выбраны с таким умыслом, чтобы ни в одной из 16bit-ячеек результат не мог превзойти 2^16.

Затем эмулируем extadd_pairwise с помощью битового сдвига, сложения и, когда необходимо, наложения битовой маски:

    ;; i32x4.extadd_pairwise_i16x8_u - not implemented
    (local.set $xtempL
      (i16x8.add                  ;; H000 HG00   F0   FE D000 DC00   B0   BA
        (i32x4.shr_u              ;;    0 H000    0   F0    0 D000    0   B0
          (local.get $xtempL)     ;; H000  G00   F0    E D000  C00   B0    A
          (i32.const 16)
        )
        (local.get $xtempL)       ;; H000  G00   F0    E D000  C00   B0    A
      )
    )

То же самое проделаем со старшими байтами регистра:

    (local.set $xtempH
      (i16x8.mul                  ;; P000 O00  N0   M  L000 K00  J0   I
        (i16x8.widen_high_i8x16_u ;;  0 P|0 O|0 N|0 M | 0 L|0 K|0 J|0 I
          (local.get $xword)      ;;  P O N M L K J I ...
        )                                                              
        (global.get $stepA_pow10) ;; 1000 100  10   1  1000 100  10   1
      )                           
    )

    (local.set $xtempH
      (i16x8.add                  ;; P000 PO00   N0   NM L000 LK00   J0   JI
        (i32x4.shr_u              ;;    0 P000    0   N0    0 L000    0   J0
          (local.get $xtempH)     ;; P000  O00   N0    M L000  K00   J0    I
          (i32.const 16)
        )
        (local.get $xtempH)       ;; P000  O00   N0    M L000  K00   J0    I
      )
    )

Упакуем оба полученных значения обратно в один регистр, отбросив лишние блоки с помощью shuffle:

    ;; shuffle
    (local.set $xword
      (i8x16.shuffle              ;; PO00   NM LK00   JI HG00   FE DC00   BA
        0x00 0x01 0x04 0x05 0x08 0x09 0x0C 0x0D 0x10 0x11 0x14 0x15 0x18 0x19 0x1C 0x1D 
        (local.get $xtempL)       ;; H000 HG00   F0   FE D000 DC00   B0   BA
        (local.get $xtempH)       ;; P000 PO00   N0   NM L000 LK00   J0   JI
      )
    )

Повторим сложение-умножение соседних блоков еще дважды - для i16x8 и i32x4, уже с наложением маски:

  (global $stepB_pow10 v128
    (v128.const i32x4 1 10000 100000000 0)
  )
  (global $stepB_mask v128
    (v128.const i32x4 0xFFFFFFFF 0x00000000 0xFFFFFFFF 0x00000000)
  )
;; ...
    (local.set $xword
      (i16x8.add                  ;; PO00 PONM LK00 LKJI HG00 HGFE DC00 DCBA
        (i32x4.shr_u              ;;    0 PO00    0 LK00    0 HG00    0 DC00
          (local.get $xword)      ;; PO00   NM LK00   JI HG00   FE DC00   BA
          (i32.const 16)
        )
        (local.get $xword)        ;; PO00   NM LK00   JI HG00   FE DC00   BA
      )
    )

    (local.set $xword
      (v128.and                   ;;    0 PONM    0 LKJI    0 HGFE    0 DCBA
        (local.get $xword)        ;; PO00 PONM LK00 LKJI HG00 HGFE DC00 DCBA
        (global.get $stepA_mask)  ;; 0000 FFFF 0000 FFFF 0000 FFFF 0000 FFFF 0x...
      )
    )

    (local.set $xword
      (i32x4.mul                  ;;         0 LKJI00000000  HGFE0000     DCBA
        (local.get $xword)        ;;    0 PONM       0 LKJI    0 HGFE   0 DCBA
        (global.get $stepB_pow10) ;;         0    100000000     10000        1
      )                           
    )

    (local.set $xword
      (i32x4.add                  ;;         0 LKJI00000000  HGFE0000 HGFEDCBA
        (i64x2.shr_u              ;;         0            0         0 HGFE0000
          (local.get $xword)      ;;         0 LKJI00000000  HGFE0000     DCBA
          (i32.const 32)
        )
        (local.get $xword)        ;;         0 LKJI00000000  HGFE0000     DCBA
      )
    )

    (local.set $xword
      (v128.and                   ;;         0 LKJI00000000         0 HGFEDCBA
        (local.get $xword)        ;;         0 LKJI00000000  HGFE0000 HGFEDCBA
        (global.get $stepB_mask)  ;;  00000000     FFFFFFFF  00000000 FFFFFFFF 0x...
      )
    )

Заметим, что самый старший блок цифр 'PONM' мы потеряли при умножениях, поскольку он не влез в размерность i32. Если все-таки предполагаются значения такой длины, данный сегмент можно вытащить отдельно через extract_lane.

Полный код SIMD-версии
(module
  (import "js" "memory" (memory 1))

  ;; текущее смещение "курсора"
  (global $off (import "js" "off") (mut i32))
  ;; текущее значение
  (global $val (mut i64)
    (i64.const 0)
  )
  (global $mask (mut i32)
    (i32.const 0)
  )
  (global $state (mut i32)
    (i32.const 0)
  )
  (global $key (mut i32)
    (i32.const 0)
  )

  (global $stepA_pow10 v128
    (v128.const i16x8 1 10 100 1000 1 10 100 1000)
  )
  (global $stepB_pow10 v128
    (v128.const i32x4 1 10000 100000000 0)
  )

  (global $stepA_mask v128
    (v128.const i16x8 0xFFFF 0x0000 0xFFFF 0x0000 0xFFFF 0x0000 0xFFFF 0x0000)
  )
  (global $stepB_mask v128
    (v128.const i32x4 0xFFFFFFFF 0x00000000 0xFFFFFFFF 0x00000000)
  )
  (global $all_zero v128
    (v128.const i8x16 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30)
  )
  (global $all_nine v128
    (v128.const i8x16 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09)
  )
  (global $shuf_reverse v128
    (v128.const i8x16 0x0F 0x0E 0x0D 0x0C 0x0B 0x0A 0x09 0x08 0x07 0x06 0x05 0x04 0x03 0x02 0x01 0x00)
  )
  (global $all_digit v128
    (v128.const i8x16 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F)
  )

  ;; таблица косвенной адресации функций
  (table 128 anyfunc)
  (elem (i32.const 0)
    $null ;; 00
    $null ;;  1
    $null ;;  2
    $null ;;  3
    $null ;;  4
    $null ;;  5
    $null ;;  6
    $null ;;  7
    $null ;;  8
    $null ;;  9
    $null ;;  A
    $null ;;  B
    $null ;;  C
    $null ;;  D
    $null ;;  E
    $null ;;  F
    $null ;; 10
    $null ;;  1
    $null ;;  2
    $null ;;  3
    $null ;;  4
    $null ;;  5
    $null ;;  6
    $null ;;  7
    $null ;;  8
    $null ;;  9
    $null ;;  A
    $null ;;  B
    $null ;;  C
    $null ;;  D
    $null ;;  E
    $null ;;  F
    $null ;; 20
    $null ;;  1
    $null ;;  2
    $null ;;  3
    $null ;;  4
    $null ;;  5
    $null ;;  6
    $null ;;  7
    $null ;;  8
    $null ;;  9
    $null ;;  A
    $null ;;  B
    $null ;;  C
    $null ;;  D
    $null ;;  E
    $null ;;  F
    $null ;; 30
    $null ;;  1
    $null ;;  2
    $null ;;  3
    $null ;;  4
    $null ;;  5
    $null ;;  6
    $null ;;  7
    $null ;;  8
    $null ;;  9
    $null ;;  A
    $null ;;  B
    $null ;;  C
    $null ;;  D
    $null ;;  E
    $null ;;  F
    $null ;; 40
    $null ;;  1
    $null ;;  2
    $null ;;  3
    $null ;;  4
    $null ;;  5
    $null ;;  6
    $null ;;  7
    $null ;;  8
    $null ;;  9
    $null ;;  A
    $null ;;  B
    $null ;;  C
    $null ;;  D
    $null ;;  E
    $null ;;  F
    $null ;; 50
    $null ;;  1
    $null ;;  2
    $null ;;  3
    $null ;;  4
    $null ;;  5
    $null ;;  6
    $null ;;  7
    $null ;;  8
    $null ;;  9
    $null ;;  A
    $null ;;  B
    $null ;;  C
    $null ;;  D
    $null ;;  E
    $null ;;  F
    $null ;; 60
    $null ;;  1
    $null ;;  2
    $null ;;  3
    $step_d ;;  4
    $null ;;  5
    $null ;;  6
    $null ;;  7
    $step_h ;;  8
    $null ;;  9
    $null ;;  A
    $null ;;  B
    $step_l ;;  C
    $null ;;  D
    $null ;;  E
    $null ;;  F
    $null ;; 70
    $null ;;  1
    $step_r ;;  2
    $step_s ;;  3
    $step_t ;;  4
    $null ;;  5
    $null ;;  6
    $step_w ;;  7
    $null ;;  8
    $null ;;  9
    $null ;;  A
    $null ;;  B
    $null ;;  C
    $null ;;  D
    $null ;;  E
    $null ;;  F
  )

  ;; обновляем маску атрибутов и значение
  (func $setData
    ;; buf[key << 3] = val
    (i64.store
      (i32.shl
        (global.get $key)
        (i32.const 3)
      )
      (global.get $val)
    )
    ;; mask |= 1 << key
    (global.set $mask
      (i32.or
        (global.get $mask)
        (i32.shl
          (i32.const 1)
          (global.get $key)
        )
      )
    )
  )

  (func $step (param $off_v i32)
    ;; $off += $off_v
    (global.set $off
      (i32.add
        (global.get $off)
        (local.get $off_v)
      )
    )
  )

  ;; разбор числа - SIMD
  (func $parseNumber
    (local $ch i32)
    (local $xword v128)
    (local $xn i32)
    (local $xtempL v128)
    (local $xtempH v128)

    ;; xword = line[off..+15]
    (local.set $xword
      (v128.load align=4
        (global.get $off)
      )
    )

    ;; xn = num digits
    (local.set $xn
      (i32.ctz
        (i32.xor
          (i8x16.bitmask
            (i8x16.le_u
              (i8x16.sub
                (local.get $xword)
                (global.get $all_zero)
              )
              (global.get $all_nine)
            )
          )
          (i32.const 0xFFFFFFFF)
        )
      )
    )

    ;; ch = xword[xn]
    (local.set $ch           ;; ch = 0x20
      (i8x16.extract_lane_u  ;; xmm[0]
        0                    ;; константа
        (i8x16.swizzle       ;; 0x... 20 20 20 20 20 20
          (local.get $xword) ;; 0x... 20 35 34 33 32 31
          (i8x16.splat       ;; 0x... 05 05 05 05 05 05
            (local.get $xn)  ;; xn = 5
          )
        )
      )
    )

    ;; clean & reverse
    (local.set $xword
      (v128.and                        ;; 0x... 00 01 02 03 04 05
        (i8x16.swizzle                 ;; 0x... 00 31 32 33 34 35
          (local.get $xword)           ;; 0x... 20 35 34 33 32 31
          (i8x16.sub                   ;; 0x... FF 00 01 02 03 04
            (global.get $shuf_reverse) ;; 0x... 0A 0B 0C 0D 0E 0F
            (i8x16.splat               ;; 0x... 0B 0B 0B 0B 0B 0B
              (i32.sub                 ;; = 0x0B
                (i32.const 16)
                (local.get $xn)        ;; xn = 5
              )
            )
          )
        )
        (global.get $all_digit)        ;; 0x... 0F 0F 0F 0F 0F 0F
      )
    )

    ;; i16x8.extmul_low_i8x16_u - not implemented
    (local.set $xtempL
      (i16x8.mul                  ;; H000 G00  F0   E  D000 C00  B0   A
        (i16x8.widen_low_i8x16_u  ;;  0 H|0 G|0 F|0 E | 0 D|0 C|0 B|0 A
          (local.get $xword)      ;;                ... H G F E D C B A
        )
        (global.get $stepA_pow10) ;; 1000 100  10   1  1000 100  10   1
      )
    )

    ;; i32x4.extadd_pairwise_i16x8_u - not implemented
    (local.set $xtempL
      (i16x8.add                  ;; H000 HG00   F0   FE D000 DC00   B0   BA
        (i32x4.shr_u              ;;    0 H000    0   F0    0 D000    0   B0
          (local.get $xtempL)     ;; H000  G00   F0    E D000  C00   B0    A
          (i32.const 16)
        )
        (local.get $xtempL)       ;; H000  G00   F0    E D000  C00   B0    A
      )
    )
    
    (local.set $xtempH
      (i16x8.mul                  ;; P000 O00  N0   M  L000 K00  J0   I
        (i16x8.widen_high_i8x16_u ;;  0 P|0 O|0 N|0 M | 0 L|0 K|0 J|0 I
          (local.get $xword)      ;;  P O N M L K J I ...
        )                                                              
        (global.get $stepA_pow10) ;; 1000 100  10   1  1000 100  10   1
      )                           
    )

    (local.set $xtempH
      (i16x8.add                  ;; P000 PO00   N0   NM L000 LK00   J0   JI
        (i32x4.shr_u              ;;    0 P000    0   N0    0 L000    0   J0
          (local.get $xtempH)     ;; P000  O00   N0    M L000  K00   J0    I
          (i32.const 16)
        )
        (local.get $xtempH)       ;; P000  O00   N0    M L000  K00   J0    I
      )
    )

    ;; shuffle
    (local.set $xword
      (i8x16.shuffle              ;; PO00   NM LK00   JI HG00   FE DC00   BA
        0x00 0x01 0x04 0x05 0x08 0x09 0x0C 0x0D 0x10 0x11 0x14 0x15 0x18 0x19 0x1C 0x1D 
        (local.get $xtempL)       ;; H000 HG00   F0   FE D000 DC00   B0   BA
        (local.get $xtempH)       ;; P000 PO00   N0   NM L000 LK00   J0   JI
      )
    )

    (local.set $xword
      (i16x8.add                  ;; PO00 PONM LK00 LKJI HG00 HGFE DC00 DCBA
        (i32x4.shr_u              ;;    0 PO00    0 LK00    0 HG00    0 DC00
          (local.get $xword)      ;; PO00   NM LK00   JI HG00   FE DC00   BA
          (i32.const 16)
        )
        (local.get $xword)        ;; PO00   NM LK00   JI HG00   FE DC00   BA
      )
    )

    (local.set $xword
      (v128.and                   ;;    0 PONM    0 LKJI    0 HGFE    0 DCBA
        (local.get $xword)        ;; PO00 PONM LK00 LKJI HG00 HGFE DC00 DCBA
        (global.get $stepA_mask)  ;; 0000 FFFF 0000 FFFF 0000 FFFF 0000 FFFF 0x...
      )
    )

    (local.set $xword
      (i32x4.mul                  ;;         0 LKJI00000000  HGFE0000     DCBA
        (local.get $xword)        ;;    0 PONM       0 LKJI    0 HGFE   0 DCBA
        (global.get $stepB_pow10) ;;         0    100000000     10000        1
      )                           
    )

    (local.set $xword
      (i32x4.add                  ;;         0 LKJI00000000  HGFE0000 HGFEDCBA
        (i64x2.shr_u              ;;         0            0         0 HGFE0000
          (local.get $xword)      ;;         0 LKJI00000000  HGFE0000     DCBA
          (i32.const 32)
        )
        (local.get $xword)        ;;         0 LKJI00000000  HGFE0000     DCBA
      )
    )

    (local.set $xword
      (v128.and                   ;;         0 LKJI00000000         0 HGFEDCBA
        (local.get $xword)        ;;         0 LKJI00000000  HGFE0000 HGFEDCBA
        (global.get $stepB_mask)  ;;  00000000     FFFFFFFF  00000000 FFFFFFFF 0x...
      )
    )

    ;; return
    (global.set $val
      (i64.add
        (i64x2.extract_lane
          0
          (local.get $xword)
        )
        (i64x2.extract_lane
          1
          (local.get $xword)
        )
      )
    )

    ;; шагаем на n символов
    ;; ch == '\n' || ch == ' ' => +1
    ;; ch == ','               => +2
    (call $step
      (i32.add
        (local.get $xn)
        (i32.add
          (i32.const 1)
          (i32.eq
            (local.get $ch)
            (i32.const 0x2C)
          )
        )
      )
    )
  )

  ;; [state, off] = [state_v, off + off_v]
  (func $iterate0 (param $state_v i32) (param $off_v i32)
    ;; state = state_v
    (global.set $state
      (local.get $state_v)
    )
    ;; off += off_v
    (call $step
      (local.get $off_v)
    )
  )

  ;; [key, off] = [state + state_v, off + off_v]
  (func $iterate1 (param $state_v i32) (param $off_v i32)
    ;; key = state + state_v
    (global.set $key
      (i32.add
        (global.get $state)
        (local.get $state_v)
      )
    )
    ;; off += off_v
    (call $step
      (local.get $off_v)
    )
  )

  (func $null (result i32)
    i32.const 0
  )

  (func $step_s (result i32) ;; shared
    ;; $state = 0
    ;; $off += 7
    (call $iterate0
      (i32.const 0)
      (i32.const 7)
    )

    i32.const 1
  )

  (func $step_l (result i32) ;; local
    ;; $state = 4
    ;; $off += 6
    (call $iterate0
      (i32.const 4)
      (i32.const 6)
    )

    i32.const 1
  )

  (func $step_t (result i32) ;; temp
    ;; $state = 8
    ;; $off += 5
    (call $iterate0
      (i32.const 8)
      (i32.const 5)
    )

    i32.const 1
  )

  (func $step_h (result i32) ;; hit
    ;; key = state + 0;
    ;; $off += 4
    (call $iterate1
      (i32.const 0)
      (i32.const 4)
    )
    ;;
    call $parseNumber
    call $setData

    i32.const 1
  )

  (func $step_r (result i32) ;; read
    ;; key = state + 1;
    ;; $off += 5
    (call $iterate1
      (i32.const 1)
      (i32.const 5)
    )
    ;;
    call $parseNumber
    call $setData

    i32.const 1
  )

  (func $step_d (result i32) ;; dirtied
    ;; key = state + 2;
    ;; $off += 8
    (call $iterate1
      (i32.const 2)
      (i32.const 8)
    )
    ;;
    call $parseNumber
    call $setData

    i32.const 1
  )

  (func $step_w (result i32) ;; written
    ;; key = state + 3;
    ;; $off += 8
    (call $iterate1
      (i32.const 3)
      (i32.const 8)
    )
    ;;
    call $parseNumber
    call $setData

    i32.const 1
  )

  (func (export "parseBuffers") (result i32)
    ;; mask = 0
    (global.set $mask
      (i32.const 0)
    )
    ;; off += 'Buffers: '.length
    (global.set $off
      (i32.add
        (global.get $off)
        (i32.const 9)
      )
    )
    ;; for (...)
    (block
      (loop
        ;; if (table[line[off]]()) continue;
        (br_if 0
          (call_indirect (result i32)
            (i32.load8_u
              (global.get $off)
            )
          )
        )
        ;; break, end loop
      )
    )

    ;; сохраняем текущее смещение
    (i32.store
      (i32.const 0xFFFC) ;; 16383 * 4
      (global.get $off)
    )
    ;; возвращаем маску
    global.get $mask
  )
)

Запускаем! Увы, наши ожидания не оправдались - время ухудшилось с 26ms до 30ms. Неужели мы написали такую плохую реализацию конвертации чисел?

Давайте выключим все эти вычисления, оставив лишь вычисление количества цифр xn и стоп-символа ch - и все равно уже 27ms!

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

Так что SIMD - полезно, но не всегда!


Заметки на полях

Можно ли ускорить приведенный выше алгоритм? Конечно!

  1. Число можно парсить в v128, даже если оно находится не в самом начале, если использовать i8x16.add_sat_u для добавления смещения.

  2. Вычленить ключевые символы (s, l, t, h, r, d, w) можно прямо в xword, если сначала получить битовую маску "послепробельных" символов, а затем конвертировать ее в позиционное представление нужных i8-блоков.

  3. Вместо таблицы функций можно использовать таблицу соответствий "ключевой символ" => {тип, длина, значение}.

Полный код buffers-test.js
const { readFileSync } = require('fs');

const fn = 'buffers-test';

const run = async () => {
  const buffer = readFileSync(`${fn}.wasm`);
  const module = await WebAssembly.compile(buffer);
  
  const memory = new WebAssembly.Memory({initial : 1});
  
  const data = readFileSync('buffers.txt');
  memory.grow((data.byteLength >> 16) + 8);
  const prj32 = new Uint32Array(memory.buffer);
  const prj8 = new Uint8Array(memory.buffer);

  // единственный раз передадим исходное смещение через переменную
  const off = new WebAssembly.Global({value : 'i32', mutable : true}, 0x80000);

  data.copy(prj8, off.value);
  // дописываем отсутствующий '\n' у последней строки
  prj8[off.value + data.length] = 0x0A;

  const importObject = {
    js : {
      memory
    , off
    }
  };

  // 10^N для домножения первого сегмента
  let pow10 = 1;
  for (let i = 0; i < 8; i++) {
    prj32[(i << 1) + 32] = pow10;
    pow10 *= 10;
  }

  // таблица соответствий ключевых символов
  let keys = {
    's' : 0x010700 // 'shared '
  , 'l' : 0x010604 // 'local '
  , 't' : 0x010508 // 'temp '
  , 'h' : 0x000400 // 'hit='
  , 'r' : 0x000501 // 'read='
  , 'd' : 0x000802 // 'dirtied='
  , 'w' : 0x000803 // 'written='
  };
  Object.entries(keys).forEach(([key, val]) => prj32[48 + key.charCodeAt()] = val);

  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
  const clz = Math.clz32;
  const ctz = integer => {
    integer |= integer << 16;
    integer |= integer << 8;
    integer |= integer << 4;
    integer |= integer << 2;
    integer |= integer << 1;
    return 32 - clz(~integer) | 0;
  };

  // https://stackoverflow.com/questions/43122082/efficiently-count-the-number-of-bits-in-an-integer-in-javascript/43122214
  const popcnt = n => {
    n = n - ((n >> 1) & 0x55555555)
    n = (n & 0x33333333) + ((n >> 2) & 0x33333333)
    return ((n + (n >> 4) & 0xF0F0F0F) * 0x1010101) >> 24
  };

  // обратная битовая маска
  for (let i = 0; i < (1 << 16); i++) {
    if (popcnt(i) < 5) {
      let r = 0xFFFFFFFF;
      for (let v = i; v; ) {
        let bp = 31 - clz(v);
        r <<= 8;
        r |= bp;
        v ^= 1 << bp;
      }
      prj32[16384 + i] = r;
    }
  }

  const instance = await WebAssembly.instantiate(module, importObject);
  const parseBuffersWASM = instance.exports.parseBuffers;

  const buffersKeys = ['shared-hit', 'shared-read', 'shared-dirtied', 'shared-written', 'local-hit', 'local-read', 'local-dirtied', 'local-written', 'temp-hit', 'temp-read', 'temp-dirtied', 'temp-written'];
  const m32to64 = 0x100000000;

  const parseBuffers = () => {
    let mask = parseBuffersWASM();
    switch (mask) {
      case 1:
        return {'shared-hit'  : prj32[0] + prj32[1] * m32to64};
      case 2:
        return {'shared-read' : prj32[2] + prj32[3] * m32to64};
      case 3:
        return {
          'shared-hit'  : prj32[0] + prj32[1] * m32to64
        , 'shared-read' : prj32[2] + prj32[3] * m32to64
        };
      default:
        let rv = {};
        for (let i = 0; mask; mask >>= 1, i++) {
          if (mask & 1) {
            let off = i << 1;
            rv[buffersKeys[i]] = prj32[off] + prj32[off + 1] * m32to64;
          }
        }
        return rv;
    }
  };

  for (let lim = data.length + off.value; prj32[16383] < lim; ) {
    let obj = parseBuffers();
  }
  off.value = 0x80000;
  prj32[16383] = off.value;

  const hrb = process.hrtime();
  // -- 8< --
  for (let lim = data.length + off.value; prj32[16383] < lim; ) {
    let obj = parseBuffers();
  }
  // -- 8< --
  const hre = process.hrtime(hrb);
  const usec = hre[0] * 1e+9 + hre[1];
  console.log(usec);
};

run();
Полный код buffers-test.wat
(module
  (import "js" "func" (func $js_func (param i32)))
  (import "js" "memory" (memory 1))

  ;; текущее смещение "курсора"
  (global $off (import "js" "off") (mut i32))
  ;; текущее значение
  (global $val (mut i64)
    (i64.const 0)
  )

  (global $stepA_pow10 v128
    (v128.const i16x8 1 10 100 1000 1 10 100 1000)
  )
  (global $stepB_pow10 v128
    (v128.const i32x4 1 10000 100000000 0)
  )

  (global $stepA_mask v128
    (v128.const i16x8 0xFFFF 0x0000 0xFFFF 0x0000 0xFFFF 0x0000 0xFFFF 0x0000)
  )
  (global $stepB_mask v128
    (v128.const i32x4 0xFFFFFFFF 0x00000000 0xFFFFFFFF 0x00000000)
  )
  (global $all_zero v128
    (v128.const i8x16 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30 0x30)
  )
  (global $all_nine v128
    (v128.const i8x16 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09 0x09)
  )
  (global $shuf_straight v128
    (v128.const i8x16 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F)
  )
  (global $shuf_reverse v128
    (v128.const i8x16 0x0F 0x0E 0x0D 0x0C 0x0B 0x0A 0x09 0x08 0x07 0x06 0x05 0x04 0x03 0x02 0x01 0x00)
  )
  (global $all_digit v128
    (v128.const i8x16 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F 0x0F)
  )
  (global $all_space v128
    (v128.const i8x16 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20 0x20)
  )
  (global $all_0A v128
    (v128.const i8x16 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A 0x0A)
  )

  ;; обновляем маску атрибутов и значение в памяти
  (func $setData (param $key i32) (param $mask i32) (result i32)
    ;; buf[key << 3] = val
    (i64.store
      (i32.shl
        (local.get $key)
        (i32.const 3)
      )
      (global.get $val)
    )
    ;; mask |= 1 << key
    (i32.or
      (local.get $mask)
      (i32.shl
        (i32.const 1)
        (local.get $key)
      )
    )
  )

  ;; разбор числа - SIMD
  (func $parseNumber (param $xword v128) (param $off i32) (result i32)
    (local $xn i32)
    (local $xtempL v128)
    (local $xtempH v128)

    ;; xn = num digits
    (local.set $xn
      (i32.ctz                           ;;           ^ = 5
        (i32.xor                         ;; 0b...     1  0  0  0  0  0
          (i32.shr_u                     ;; 0b...     0  1  1  1  1  1
            (i8x16.bitmask               ;; 0b...  0  1  1  1  1  1  0
              (i8x16.le_u                ;; 0x... 00 FF FF FF FF FF 00
                (i8x16.sub               ;; 0x... F0 05 04 03 02 01 0D
                  (local.get $xword)     ;; 0x... 20 35 34 33 32 31 3D
                  (global.get $all_zero) ;; 0x... 30 30 30 30 30 30 30
                )
                (global.get $all_nine)   ;; 0x... 09 09 09 09 09 09 09
              )
            )
            (local.get $off)             ;; off = 1
          )
          (i32.const 0xFFFFFFFF)         ;; 0b...  1  1  1  1  1  1  1
        )
      )
    )

    ;; clean & reverse
    (local.set $xword
      (v128.and                          ;; 0x... 00 00 01 02 03 04 05
        (i8x16.swizzle                   ;; 0x... 00 00 31 32 33 34 35
          (local.get $xword)             ;; 0x... 20 35 34 33 32 31 3D
          (i8x16.add_sat_u               ;; 0x... FF FF 01 02 03 04 05
            (i8x16.sub                   ;; 0x... FF FF 00 01 02 03 04
              (global.get $shuf_reverse) ;; 0x... 09 0A 0B 0C 0D 0E 0F
              (i8x16.splat               ;; 0x... 0B 0B 0B 0B 0B 0B 0B
                (i32.sub                 ;; = 0x0B
                  (i32.const 16)
                  (local.get $xn)        ;; xn = 5
                )
              )
            )
            (i8x16.splat                 ;; 0x... 01 01 01 01 01 01 01
              (local.get $off)           ;; off = 1
            )
          )
        )
        (global.get $all_digit)          ;; 0x... 0F 0F 0F 0F 0F 0F 0F
      )
    )

    ;; i16x8.extmul_low_i8x16_u - not implemented
    (local.set $xtempL
      (i16x8.mul                  ;; H000 G00  F0   E  D000 C00  B0   A
        (i16x8.widen_low_i8x16_u  ;;  0 H|0 G|0 F|0 E | 0 D|0 C|0 B|0 A
          (local.get $xword)      ;;                ... H G F E D C B A
        )
        (global.get $stepA_pow10) ;; 1000 100  10   1  1000 100  10   1
      )
    )

    ;; i32x4.extadd_pairwise_i16x8_u - not implemented
    (local.set $xtempL
      (i16x8.add                  ;; H000 HG00   F0   FE D000 DC00   B0   BA
        (i32x4.shr_u              ;;    0 H000    0   F0    0 D000    0   B0
          (local.get $xtempL)     ;; H000  G00   F0    E D000  C00   B0    A
          (i32.const 16)
        )
        (local.get $xtempL)       ;; H000  G00   F0    E D000  C00   B0    A
      )
    )
    
    (local.set $xtempH
      (i16x8.mul                  ;; P000 O00  N0   M  L000 K00  J0   I
        (i16x8.widen_high_i8x16_u ;;  0 P|0 O|0 N|0 M | 0 L|0 K|0 J|0 I
          (local.get $xword)      ;;  P O N M L K J I ...
        )                                                              
        (global.get $stepA_pow10) ;; 1000 100  10   1  1000 100  10   1
      )                           
    )

    (local.set $xtempH
      (i16x8.add                  ;; P000 PO00   N0   NM L000 LK00   J0   JI
        (i32x4.shr_u              ;;    0 P000    0   N0    0 L000    0   J0
          (local.get $xtempH)     ;; P000  O00   N0    M L000  K00   J0    I
          (i32.const 16)
        )
        (local.get $xtempH)       ;; P000  O00   N0    M L000  K00   J0    I
      )
    )

    ;; shuffle
    (local.set $xword
      (i8x16.shuffle              ;; PO00   NM LK00   JI HG00   FE DC00   BA
        0x00 0x01 0x04 0x05 0x08 0x09 0x0C 0x0D 0x10 0x11 0x14 0x15 0x18 0x19 0x1C 0x1D 
        (local.get $xtempL)       ;; H000 HG00   F0   FE D000 DC00   B0   BA
        (local.get $xtempH)       ;; P000 PO00   N0   NM L000 LK00   J0   JI
      )
    )

    (local.set $xword
      (i16x8.add                  ;; PO00 PONM LK00 LKJI HG00 HGFE DC00 DCBA
        (i32x4.shr_u              ;;    0 PO00    0 LK00    0 HG00    0 DC00
          (local.get $xword)      ;; PO00   NM LK00   JI HG00   FE DC00   BA
          (i32.const 16)
        )
        (local.get $xword)        ;; PO00   NM LK00   JI HG00   FE DC00   BA
      )
    )

    (local.set $xword
      (v128.and                   ;;    0 PONM    0 LKJI    0 HGFE    0 DCBA
        (local.get $xword)        ;; PO00 PONM LK00 LKJI HG00 HGFE DC00 DCBA
        (global.get $stepA_mask)  ;; 0000 FFFF 0000 FFFF 0000 FFFF 0000 FFFF 0x...
      )
    )

    (local.set $xword
      (i32x4.mul                  ;;         0 LKJI00000000  HGFE0000     DCBA
        (local.get $xword)        ;;    0 PONM       0 LKJI    0 HGFE   0 DCBA
        (global.get $stepB_pow10) ;;         0    100000000     10000        1
      )                           
    )

    (local.set $xword
      (i32x4.add                  ;;         0 LKJI00000000  HGFE0000 HGFEDCBA
        (i64x2.shr_u              ;;         0            0         0 HGFE0000
          (local.get $xword)      ;;         0 LKJI00000000  HGFE0000     DCBA
          (i32.const 32)
        )
        (local.get $xword)        ;;         0 LKJI00000000  HGFE0000     DCBA
      )
    )

    (local.set $xword
      (v128.and                   ;;         0 LKJI00000000         0 HGFEDCBA
        (local.get $xword)        ;;         0 LKJI00000000  HGFE0000 HGFEDCBA
        (global.get $stepB_mask)  ;;  00000000     FFFFFFFF  00000000 FFFFFFFF 0x...
      )
    )

    (global.set $val
      (i64.add
        (i64x2.extract_lane
          0
          (local.get $xword)
        )
        (i64x2.extract_lane
          1
          (local.get $xword)
        )
      )
    )
    
    ;; return
    (local.get $xn)
  )

  (func (export "parseBuffers") (result i32)
    (local $ch i32)

    (local $chp i32)
    (local $chv i32)
    (local $chm i32)
    (local $chi i32)

    (local $ln i32)
    (local $chpos i32)
    (local $xword v128)
    (local $xn i32)
    (local $xbp i32)
    (local $eol i32)
    (local $esp i32)
    (local $vtemp i64)

    (local $mask i32)
    (local $state i32)
    (local $key i32)

    ;; mask = 0
    (local.set $mask
      (i32.const 0)
    )
    ;; val = 0
    (global.set $val
      (i64.const 0)
    )
    ;; off += 'Buffers: '.length
    (global.set $off
      (i32.add
        (global.get $off)
        (i32.const 9)
      )
    )
    ;; первый символ всегда "послепробельный"
    (local.set $esp
      (i32.const 1)
    )
    (local.set $xbp
      (i32.const -1)
    )
    ;; for (...)
    (block
      (loop
        ;; xword = line[off..+15]
        (local.set $xword
          (v128.load align=4
            (global.get $off)
          )
        )

        ;; xbp == -1 -> не парсим сейчас число
        ;; xbp >=  0 -> начиная отсюда и парсим
        (if
          (i32.ne
            (local.get $xbp)
            (i32.const -1)
          )
          (then
            ;; store temp
            (local.set $vtemp
              (global.get $val)
            )

            ;; xn = num digits
            (local.set $xn
              (call $parseNumber
                (local.get $xword)
                (local.get $xbp)
              )
            )

            (if
              (i32.eqz
                (local.get $xbp)
              )
              (then
                ;; val = val * 10 ^ memory[128 + (n << 3)]:i32 + vtemp
                (global.set $val
                  (i64.add
                    (i64.mul
                      (local.get $vtemp)
                      (i64.load align=4
                        (i32.add
                          (i32.shl
                            (local.get $xn)
                            (i32.const 3)
                          )
                          (i32.const 128)
                        )
                      )
                    )
                    (global.get $val)
                  )
                )
              )
            )

            ;; store value
            (local.set $mask
              (call $setData
                (local.get $key)
                (local.get $mask)
              )
            )

            ;; xbp = -1
            (local.set $xbp
              (i32.const -1)
            )
          )
        )

        ;; off += 16
        (global.set $off
          (i32.add
            (global.get $off)
            (i32.const 16)
          )
        )

        ;; формируем битовую маску "послепробельных" символов
        (local.set $chm
          (i32.and
            (i32.add
              (i32.shl
                ;; space mask
                (i8x16.bitmask
                  (i8x16.eq
                    (local.get $xword)
                    (global.get $all_space)
                  )
                )
                (i32.const 1)
              )
              ;; выставляем для первого символа, что он "послепробельный",
              ;; если пробел был последним символом предыдущего блока
              (local.get $esp)
            )
            (i32.const 0xFFFF)
          )
        )

        ;; по таблице превращаем битовую маску в позиции битов
        ;; shared hit=1 read
        ;; 1000000100000100 => 0x FF 0D 07 00
        ;; chp = memory[0x10000 + (chm << 2)]:i32
        (local.set $chp
          (i32.load align=4
            (i32.add
              (i32.const 0x10000)
              (i32.shl
                (local.get $chm)
                (i32.const 2)
              )
            )
          )
        )
        ;; превращаем позиции символов в их значения
        (local.set $chv
          (i32x4.extract_lane
            0
            (i8x16.swizzle
              (local.get $xword)
              (i32x4.replace_lane
                0
                (i8x16.splat
                  (i32.const 0xFF)
                )
                (local.get $chp)
              )
            )
          )
        )

        ;; перебираем символы согласно выставленным битам маски, пока она не обнулится
        (block
          (loop
            ;; if (chv == 0) break
            (br_if 1
              (i32.eqz
                (local.get $chv)
              )
            )

            (local.set $chpos
              (i32.and
                (local.get $chp)
                (i32.const 0xFF)
              )
            )

            ;; ch = xword[chpos]
            (local.set $ch
              (i32.and
                (local.get $chv)
                (i32.const 0xFF)
              )
            )

            ;; ch = memory[0xC0 + ch]
            (local.set $ch
              (i32.load align=4
                (i32.add
                  (i32.shl
                    (local.get $ch)
                    (i32.const 2)
                  )
                  (i32.const 0xC0)
                )
              )
            )

            ;; ln = ch >> 8
            (local.set $ln
              (i32.shr_u
                (local.get $ch)
                (i32.const 8)
              )
            )

            ;; позиция начала числа, смещение хранится в старшем байте ch
            (local.set $xbp
              (i32.and
                (i32.add
                  (local.get $chpos)
                  (local.get $ln)
                )
                (i32.const 0xFF)
              )
            )

            ;; ch & 0x10000
            (if
              (i32.eqz
                (i32.and
                  (local.get $ch)
                  (i32.const 0x10000)
                )
              )
              ;; key
              (then
                (local.set $key
                  (i32.and
                    (i32.add
                      (local.get $state)
                      (local.get $ch)
                    )
                    (i32.const 0x0F)
                  )
                )

                ;; если число начинается в следующем блоке - переходим к нему
                (if
                  (i32.ge_u
                    (local.get $xbp)
                    (i32.const 0x10)
                  )
                  (then
                    ;; val = 0
                    (global.set $val
                      (i64.const 0)
                    )
                    ;; xbp -= 16
                    (local.set $xbp
                      (i32.and
                        (local.get $xbp)
                        (i32.const 0x0F)
                      )
                    )
                    (br 1)
                  )
                )

                ;; xn = num digits
                (local.set $xn
                  (call $parseNumber
                    (local.get $xword)
                    (local.get $xbp)
                  )
                )

                ;; xbp = xbp + xn == 0x10 ? 0 : -1
                (local.set $xbp
                  (if (result i32)
                    (i32.eq
                      (i32.add
                        (local.get $xbp)
                        (local.get $xn)
                      )
                      (i32.const 0x10)
                    )
                    (then
                      (i32.const 0)
                    )
                    (else
                      ;; store value
                      (local.set $mask
                        (call $setData
                          (local.get $key)
                          (local.get $mask)
                        )
                      )
                      (i32.const -1)
                    )
                  )
                )
              )
              ;; state
              (else
                (local.set $state
                  (i32.and
                    (local.get $ch)
                    (i32.const 0x0F)
                  )
                )
              )
            )

            ;; chv >>= 8
            (local.set $chv
              (i32.shr_u
                (local.get $chv)
                (i32.const 8)
              )
            )
            ;; chp >>= 8
            (local.set $chp
              (i32.shr_u
                (local.get $chp)
                (i32.const 8)
              )
            )
            
            ;; do loop
            (br 0)
          )
        )

        ;; esp = xword[15] == 0x20
        (local.set $esp
          (i32.eq
            (i8x16.extract_lane_u
              15
              (local.get $xword)
            )
            (i32.const 0x20)
          )
        )

        ;; check EOL
        (local.set $eol
          (i8x16.bitmask
            (i8x16.eq
              (local.get $xword)
              (global.get $all_0A)
            )
          )
        )

        ;; if (!eol) break
        (br_if 0
          (i32.eqz
            (local.get $eol)
          )
        )

        ;; break, end loop

        ;; store value
        (local.set $mask
          (call $setData
            (local.get $key)
            (local.get $mask)
          )
        )

        ;; определяем позицию перевода строки
        (local.set $eol
          (i32.ctz
            (local.get $eol)
          )
        )

        ;; встаем на начало следующей строки
        ;; off += 15 - eol
        (global.set $off
          (i32.sub
            (i32.add
              (global.get $off)
              (local.get $eol)
            )
            (i32.const 15)
          )
        )

        ;; break
        (br 1)
      )
    )

    ;; сохраняем текущее смещение
    (i32.store
      (i32.const 0xFFFC) ;; 16383 * 4
      (global.get $off)
    )
    ;; возвращаем маску
    (local.get $mask)
  )
)

Увы, все эти оптимизации не позволяют обогнать SWAR-код из прошлой части, а лишь добиться паритета с ним.

В целом, для нашей конкретной задачи использование SIMD оказывается малоэффективным, поскольку расстояния в памяти между значимой информацией достаточно велики, а сама она мала. Например, для полного декодирования такой строки длиной 47 символов достаточно прочитать всего 11 из них:

Buffers: shared hit=123 read=45, temp written=6
         ^      ^   *** ^    **  ^    ^       *

Материалы:

  • WebAssembly 128-bit packed SIMD Extension

  • WASM SIMD Implementation Status

  • wat2wasm demo

  • StackOverflow: How to implement atoi using SIMD?

  • V8 blog: Fast, parallel applications with WebAssembly SIMD

Источник: https://habr.com/ru/company/tensor/blog/545642/


Интересные статьи

Интересные статьи

Работая над приложением, связанным с финансовыми операциями, возникла необходимость распознать и выделить суммы на чеках. Начиная с 13-ой версии в IOS-разработке появился...
Однажды нам поступил интересный заказ на разработку и производство партии контроллеров для управления элементами наружной рекламы (ЭНР). Они должны были устанавливаться в аптеках о...
Новая методика взлома преодолевает проблему «джиттера сети», которая может влиять на успешность атак по сторонним каналам Новая методика, разработанная исследователями Левенско...
Задача обнаружения объектов на изображении сегодня является одной из ведущих в области машинного зрения. Ее суть заключается в том, чтобы не только классифицировать объект на снимке, но и указать...
Всемирная паутина — это океан данных. Здесь можно посмотреть практически любую интересующую Вас информацию. Однако, "вытащить" эту информацию из интернета уже сложнее. Есть несколько способов п...