Javascript Производительность: Работа модуля отрицательного числа в пределах декрементирующего цикла, замедляющего код более чем на 100%

Я проходил через Красноречивый JavaScript (снова) и наткнулся на упражнение "Шахматная доска" главы 2. У меня была моя одна достойная версия решения, написанная в тот же день, когда я впервые ее читал, а другая версия решения была представлена ​​в Веб-сайт Elequent Javascript. Я один из новичков, которые хотят быть суперэффективными программистами с одним вопросом в голове: "Могу ли я заставить его работать чуть быстрее или меньше?"

Итак, во время моего поиска в Интернете несколько месяцев назад я встретил вопрос о переполнении стека, в отношении цикла for loop vs while на основе производительности. Поскольку в этом потоке упоминалось for, циклы медленнее, чем while, а циклы с декрементирующим итератором быстрее, поэтому я переписал код для лучшей производительности.

Здесь новая версия с for заменена на while и условия, отредактированные для декрементизации:

console.time("looping");
var gridSize = 5000, str = '', i = gridSize, j;
while (i--) {
  j = gridSize;
  while (j--) {
    if ((i - j) % 2 === 0)
      str += " ";
    else
      str += "#";
  }
  str += "\n";
}

//console.log(str);
console.timeEnd("looping");

Ответ 1

Это далеко не полный ответ и требует дальнейшего изучения (или понимания того, кто знает подробности реализации V8). Тем не менее, вот мои выводы:

Sidenode: результаты были собраны с использованием Node.JS с использованием следующей командной строки:

node --expose-gc --print-code --code-comments --print-opt-code --trace-hydrogen - directirect-code-traces --redirect-code-traces-to = code.asm --trace_representation --trace-deopt --trace-opt 1.js

и немного смотреть в исходный код V8.

1. Разница в производительности связана с тем, что в этом случае генерируется другой машинный код. Для + код для % равен

                  ;;; <@134,#123> add-i
00000151A32DD74B   395  03c2           addl rax,rdx
00000151A32DD74D   397  0f807a030000   jo 1293  (00000151A32DDACD)
                  ;;; <@136,#126> mod-by-power-of-2-i
00000151A32DD753   403  85c0           testl rax,rax
00000151A32DD755   405  790f           jns 422  (00000151A32DD766)
00000151A32DD757   407  f7d8           negl rax
00000151A32DD759   409  83e001         andl rax,0x1
00000151A32DD75C   412  f7d8           negl rax
00000151A32DD75E   414  0f846e030000   jz 1298  (00000151A32DDAD2)
00000151A32DD764   420  eb03           jmp 425  (00000151A32DD769)
00000151A32DD766   422  83e001         andl rax,0x1
                  ;;; <@138,#200> smi-tag
00000151A32DD769   425  8bd8           movl rbx,rax
00000151A32DD76B   427  48c1e320       REX.W shlq rbx, 32
                  ;;; <@140,#130> gap
00000151A32DD76F   431  488bc2         REX.W movq rax,rdx

а для - код намного сложнее:

                  ;;; <@136,#123> sub-i
00000151A32E57E1   417  412bc3         subl rax,r11
00000151A32E57E4   420  0f8039040000   jo 1507  (00000151A32E5C23)
                  ;;; <@138,#200> int32-to-double
00000151A32E57EA   426  c5f957c0       vxorpd xmm0,xmm0,xmm0
00000151A32E57EE   430  c5fb2ac0       vcvtlsi2sd xmm0,xmm0,rax
                  ;;; <@139,#200> gap
00000151A32E57F2   434  c5f928ca       vmovapd xmm1,xmm2
                  ;;; <@140,#126> mod-d
00000151A32E57F6   438  4989e2         REX.W movq r10,rsp
00000151A32E57F9   441  4883ec28       REX.W subq rsp,0x28
00000151A32E57FD   445  4883e4f0       REX.W andq rsp,0xf0
00000151A32E5801   449  4c89542420     REX.W movq [rsp+0x20],r10
00000151A32E5806   454  48b830d4124001000000 REX.W movq rax,000000014012D430
00000151A32E5810   464  ffd0           call rax
00000151A32E5812   466  488b642420     REX.W movq rsp,[rsp+0x20]
                  ;;; <@142,#126> lazy-bailout
                  ;;; <@144,#202> number-tag-d
00000151A32E5817   471  498b9dc06f0400 REX.W movq rbx,[r13+0x46fc0]
00000151A32E581E   478  488bc3         REX.W movq rax,rbx
00000151A32E5821   481  4883c010       REX.W addq rax,0x10
00000151A32E5825   485  493b85c86f0400 REX.W cmpq rax,[r13+0x46fc8]
00000151A32E582C   492  0f878f030000   ja 1409  (00000151A32E5BC1)
00000151A32E5832   498  498985c06f0400 REX.W movq [r13+0x46fc0],rax
00000151A32E5839   505  48ffc3         REX.W incq rbx
00000151A32E583C   508  4d8b5560       REX.W movq r10,[r13+0x60]
00000151A32E5840   512  4c8953ff       REX.W movq [rbx-0x1],r10
00000151A32E5844   516  c5fb114307     vmovsd [rbx+0x7],xmm0
                  ;;; <@146,#130> gap
00000151A32E5849   521  488b45a0       REX.W movq rax,[rbp-0x60]
00000151A32E584D   525  488b7db8       REX.W movq rdi,[rbp-0x48]
00000151A32E5851   529  488b75c0       REX.W movq rsi,[rbp-0x40]
00000151A32E5855   533  488b4dc8       REX.W movq rcx,[rbp-0x38]
00000151A32E5859   537  488b55b0       REX.W movq rdx,[rbp-0x50]
00000151A32E585D   541  4c8b4da8       REX.W movq r9,[rbp-0x58]
00000151A32E5861   545  4c8b4598       REX.W movq r8,[rbp-0x68]
00000151A32E5865   549  c5fb109578ffffff vmovsd xmm2,[rbp-0x88]

Короче говоря, разница в том, что для случая "плюс" операция Mod (%) выполняется с использованием высокоспециализированного машинного кода mod-by-power-of-2-i, но для случая "минус" это делается с помощью mod-d, который является плавающей запятой на основе арифметической реализации.

Обратите внимание, что mod-by-power-of-2-i машинный код поддерживает отрицательные значения. Его можно грубо переписать следующим образом:

if (rax < 0) {
    rax = -rax;
    rax = rax & 1;
    rax = -rax;
}
else {
    rax = rax & 1;
}

Так что это не пример оптимизированного машинного кода только для положительных значений.

2. Разница в сгенерированном коде, похоже, исходит из того, что вывод типа работает по-разному. Журналы, созданные --trace_representation, показывают следующую разницу между случаями "плюс" и "минус" для упрощенной программы:

var f_minus = function(log) {
    var str = '', i = gridSize, j;
    while (i--) {
      j = gridSize;
      while (j--) {
        var ttt = (i - j) % 2
      }
    }

  if(log) {
     if(ttt == -1)
        console.log(t);
   }
}


var f_plus = function(log) {
    var str = '', i = gridSize, j;
    while (i--) {
      j = gridSize;
      while (j--) {
        var ttt = (i + j) % 2
      }
    }

    if(log){
     if(ttt == -1)
        console.log(t);
    }
}

Сравнить

[marking 00000025D4303E91 <JS Function f_minus (SharedFunctionInfo 00000278933F61C1)> for optimized recompilation, reason: small function, ICs with typeinfo: 8/12 (66%), generic ICs: 0/12 (0%)]
[compiling method 00000025D4303E91 <JS Function f_minus (SharedFunctionInfo 00000278933F61C1)> using Crankshaft OSR]
#37 Phi is used by real #110 Branch as v
#38 Phi is used by real #58 Add as t
#38 Phi is used by real #69 StackCheck as v
#38 Phi is used by real #70 LoadContextSlot as v
#38 Phi is used by real #122 CompareGeneric as t
#38 Phi is used by real #132 LoadGlobalGeneric as v
#38 Phi is used by real #134 LoadNamedGeneric as v
#38 Phi is used by real #136 LoadGlobalGeneric as v
#38 Phi is used by real #141 CallWithDescriptor as v
#38 Phi is used by real #156 Return as v
#38 Phi is used by real #101 Mod as t
#38 Phi is used by real #98 Sub as t
#38 Phi is used by real #95 StackCheck as v
#38 Phi is used by real #84 Add as t
#47 Phi is used by real #56 ForceRepresentation as s
#49 Phi is used by real #122 CompareGeneric as t
#77 Phi is used by real #83 ForceRepresentation as s
generalizing use representation 'v' of #40 Phi with uses of #47 Phi 's'
generalizing use representation 'v' of #42 Phi with uses of #49 Phi 't'
generalizing use representation 't' of #42 Phi with uses of #78 Phi 'v'
generalizing use representation 'v' of #49 Phi with uses of #78 Phi 'v'
generalizing use representation 'v' of #78 Phi with uses of #49 Phi 't'
Changing #101 Mod representation v -> i based on inputs
Changing #101 Mod representation i -> d based on output
Changing #98 Sub representation v -> s based on inputs
Changing #98 Sub representation s -> i based on use requirements
Changing #84 Add representation v -> i based on inputs
...

к этому

[marking 000002C17CAAB341 <JS Function f_plus (SharedFunctionInfo 00000278933F6289)> for optimized recompilation, reason: small function, ICs with typeinfo: 8/12 (66%), generic ICs: 0/12 (0%)]
[compiling method 000002C17CAAB341 <JS Function f_plus (SharedFunctionInfo 00000278933F6289)> using Crankshaft OSR]
#37 Phi is used by real #110 Branch as v
#38 Phi is used by real #58 Add as t
#38 Phi is used by real #69 StackCheck as v
#38 Phi is used by real #70 LoadContextSlot as v
#38 Phi is used by real #122 CompareGeneric as t
#38 Phi is used by real #132 LoadGlobalGeneric as v
#38 Phi is used by real #134 LoadNamedGeneric as v
#38 Phi is used by real #136 LoadGlobalGeneric as v
#38 Phi is used by real #141 CallWithDescriptor as v
#38 Phi is used by real #156 Return as v
#38 Phi is used by real #101 Mod as t
#38 Phi is used by real #98 Add as t
#38 Phi is used by real #95 StackCheck as v
#38 Phi is used by real #84 Add as t
#47 Phi is used by real #56 ForceRepresentation as s
#49 Phi is used by real #122 CompareGeneric as t
#77 Phi is used by real #83 ForceRepresentation as s
generalizing use representation 'v' of #40 Phi with uses of #47 Phi 's'
generalizing use representation 'v' of #42 Phi with uses of #49 Phi 't'
generalizing use representation 't' of #42 Phi with uses of #78 Phi 'v'
generalizing use representation 'v' of #49 Phi with uses of #78 Phi 'v'
generalizing use representation 'v' of #78 Phi with uses of #49 Phi 't'
Changing #101 Mod representation v -> i based on inputs
Changing #98 Add representation v -> s based on inputs
Changing #98 Add representation s -> i based on use requirements
Changing #84 Add representation v -> i based on inputs
...

Интересным отличием является линия

Changing #101 Mod representation i -> d based on output

который присутствует только в случае f_minus, но не в f_plus. По какой-то причине компилятор считает, что в случае типа f_minus тип должен быть Double вместо Integer, основывающимся на предположении выходного значения. Интересно, если я изменю строку

        var ttt = (i - j) % 2

к

        var ttt = (i - j + gridSize) % 2; 

он снова начинает генерировать быстрый mod-by-power-of-2-i код. Так что да, похоже, что выходное значение влияет на оптимизацию компилятора. Но непонятно, почему это происходит в этом конкретном случае.

На первый взгляд это поведение выглядит как ошибка в оптимизационном компиляторе, который не замечает, что "минус" может обрабатываться также с помощью mod-by-power-of-2-i. Я не смог проследить, почему это происходит, просто взглянув на исходный код, поэтому дальнейший ввод приветствуется.

Ответ 2

вместо использования дорогостоящей модульной операции в

((i - j) % 2 === 0)

вы можете использовать побитовые операции

(((i-j)&1) === 0)

Как было предложено SBS в комментариях, вы также должны попробовать

(((i^j)&1) === 0)

Ответ 3

Мои тесты (в среднем по 5 прогонов каждый) в nodejs показывают

(i - j) % 2 // 1170ms
(i + j) % 2 //  720ms
Math.abs(i - j) % 2 // 720ms
Math.abs(i + j) % 2 // 720ms
(gridSize + i + j) % 2 // 715ms
(gridSize + i - j) % 2 // 710ms
(-i - j) % 2 // 1500ms

Некоторая нечеткость там, большой сюрприз - вызов Math.abs имеет практически нулевой эффект на случай i + j, но еще более удивительно, что добавление gridSize делает gridSize + i - j случай самым быстрым!!

Но то, что я могу извлечь из этого, состоит в том, что главная проблема заключается в

(i - j) % 2

Многие (i - j) являются < 0 (половина из них?)

С (-i - j) ВСЕ значения < 0

Заключение: при работе с модулем с отрицательным числом производительность значительно снижается

Заметьте, вы должны иметь возможность использовать

console.time("looping");
...
console.timeEnd("looping");

в вашем браузере, так что вы можете запустить тот же код, не используя performance.now() в браузере

Не уверен, насколько это сложно, но

console.time("positive");
(function() {
    var size = 100000;
    var v = 0;
    while (size--) {
        v+=(+size)%2
    }
})();
console.timeEnd("positive");

console.time("negative");
(function() {
    var size = 100000;
    var v = 0;
    while (size--) {
        v+=(-size)%2
    }
})();
console.timeEnd("negative");

Ответ 4

Основная проблема - вложенные циклы. Старайтесь избегать их:

console.time("looping");
var gridSize = 5000;
var board = "";
var firstLine = "";
var secondLine = "";

var counter = 0;
var isBlack = true;

while (counter < gridSize) {
    firstLine += isBlack ? "#" : " ";
    secondLine += isBlack ? " " : "#";
    isBlack = !isBlack ;
    counter++ ;
}

var counter = 0;
var isBlack = false;
while (counter < gridSize) {
    board =  board + (isBlack ? firstLine : secondLine) + "\n";
    isBlack = !isBlack;
    counter++;
}
// console.log(board);
console.timeEnd("looping");

Ответ 5

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

Мое заключение: проблема не может быть фактически отрицательным модулем, но некоторые другие части могут влиять на эталоны.

console.time("looping");

var board = [];

var size = 5000;

for (var y = 0; y < size; y++) {
  board[y]=[];
  for (var x = 0; x < size; x++) {
    if ((x + y) % 2 === 0)
       board[y][x] = " ";
    else
       board[y][x] = "#";
   }
    
}

 
console.timeEnd("looping");