Утечки памяти в простом приложении nodejs

#javascript #node.js #memory-leaks

#javascript #node.js #утечки памяти

Вопрос:

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

 var step;
var numberOfSteps;
for (var i = 0; i < 100000000000000; i  ) {
    step = i;
    numberOfSteps = 0;
    while (step !== 1) {
        if (step%2 === 0)
            step /= 2;
        else
            step = 3 * step   1;
        numberOfSteps  ;
    }
    console.log(""   i   ": "   numberOfSteps   " steps.");
}
  

Я пробовал переменные как в цикле, так и вне его. Я пытался обнулить их в конце цикла. Ничто не меняет утечку памяти.

Комментарии:

1. Где утечка? Я попробовал это на своем компьютере, и моя память увеличилась менее чем на 0,01G

2. утечки нет … но цикл while бесконечен для шага === 0 … хотя, если вы исправите эту проблему, узел, похоже, продолжает медленно поглощать память, не так ли

3. Это то console.log , что вызывает это — это почти так, как если бы GC не мог работать для очистки мусора, оставленного консолью. вызов журнала

4. Я не смог запустить его в своем случае, после 2-3 минут запуска он сломался и разбился: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory Aborted (core dumped) (дамп ядра больше). Однако я проверял использование памяти, оно увеличивалось примерно на 800 МБ, а затем оставалось стабильным, продолжало работать, и произошел сбой с дампом ядра выше.

5. Это должно начинаться с i = 1, я просто вставил туда число, на котором остановился. Выше единицы оно никогда не должно опускаться ниже 1.

Ответ №1:

Немного исследую мой дамп ядра:

 <--- Last few GCs --->

  131690 ms: Scavenge 1398.1 (1458.1) -> 1398.1 (1458.1) MB, 1.3 / 0 ms (  2.8 ms in 1 steps since last GC) [allocation failure] [incremental marking delaying mark-sweep].
  132935 ms: Mark-sweep 1398.1 (1458.1) -> 1398.1 (1458.1) MB, 1245.0 / 0 ms (  3.7 ms in 2 steps since start of marking, biggest step 2.8 ms) [last resort gc].
  134169 ms: Mark-sweep 1398.1 (1458.1) -> 1398.1 (1458.1) MB, 1234.5 / 0 ms [last resort gc].


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x33083d8e3ac1 <JS Object>
    1: /* anonymous */ [/user/projects/test.js:~1] [pc=0x557d307b271] (this=0x2a4a669d8341 <an Object with map 0xf8593408359>,exports=0x33083d804189 <undefined>,require=0x33083d804189 <undefined>,module=0x33083d804189 <undefined>,__filename=0x33083d804189 <undefined>,__dirname=0x33083d804189 <undefined>)
    3: _compile [module.js:413] [pc=0x557d304d03c] (this=0x2a4a669d8431...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory
Aborted (core dumped)
  

Похоже, что это известная проблема с console.log, согласно этой проблеме на github https://github.com/nodejs/node/issues/3171

Это известная «проблема», поскольку запись в стандартный вывод в случае tty / console выполняется асинхронно. Таким образом, очень быстрая регистрация большого количества данных вполне может привести к буферизации большого количества записей в памяти, если tty / console не может идти в ногу.

Ответ №2:

Вот код, который, кажется, достигает максимума примерно в 50 МБ для меня

Это выполняет функцию партиями по 10000 — с setImmediate для обработки следующего пакета

 function collatz(n) {
    var step,numberOfSteps, i;
    for(i = 0; i < 10000; i  , n  ) {
        step = n;
        numberOfSteps = 0;
        while (step !== 1) {
            if (step%2 === 0)
                step /= 2;
            else
                step = 3 * step   1;
            numberOfSteps  ;
        }
        console.log(""   n   ": "   numberOfSteps   " steps.");
    }
    if (n < 100000000000000) {
        setImmediate(collatz, n);
    }
}
collatz(1);
  

Обратите внимание, что в этом случае цикл for может начинаться с 0, потому что n начнется с 1: p

Я не пробовал более высокие значения цикла for

Я провел некоторый сравнительный анализ с исходным кодом — выполнение 100 за раз (в цикле for) дает ту же производительность, что и 10000, и неотличимо по производительности от исходного кода. Даже при 10 за раз я бы не стал говорить, что этот метод тоже работает медленнее. Только по 1 за раз это последовательно на 5-8% медленнее, чем исходный код

Обратите внимание, что изначально я думал, что проблема заключается в сборке мусора (или его отсутствии) из-за того, что узкий цикл не дает узлу времени на ведение домашнего хозяйства, но пока я публиковал ответ, @Svabel опубликовал то, что, по-видимому, является известной проблемой при попадании в консоль.жесткий журнал.

Я могу только предположить, что использование setImmediate позволяет вести какое-то домашнее хозяйство в отношении буферов tty, что в противном случае невозможно.

Комментарии:

1. Можете ли вы объяснить, почему асинхронность решает проблему с памятью?

2. не совсем, я думал, что это может быть проблема с GC, но, прочитав ответ @Svabel, это может быть как-то связано с «известной проблемой», касающейся console.log — я не хотел включать это в свой ответ, поскольку это казалось бы неуместным

3. интересно, я вижу в этой «проблеме», что использование setImmediate было предложенным решением — один setImmediate на итерацию — 10000 на итерацию является излишним, даже 100 дает аналогичную (на самом деле в течение нескольких тестов, я не могу сказать, что это вообще хуже) «производительность» по сравнению с исходным кодом — тогда как1 на итерацию влияет на скорость примерно на 8%