ChALkeR ยท GitHub

Note: while switching to jemalloc might be unoptimal, it could be still useful to gather information about positive and negative implications of jemalloc usage. Let's do that in this issue. I am not advocating to switch to jemalloc (yet), but imo that is worth investigation.

In some situations that I observed, it consumes slightly more memory (~5%), but it is able to significantly reduce the memory usage by orders of magnitude in some cases (basically in a subset of cases where glibc behaves significantly unoptimal).

In some situations, jemalloc consumes significantly more memory though, but appears to be faster.

Testcase 1 (based on #21967):

'use strict';
const bs = 4 * 1024 * 1024; // 4 MiB
const retained = [];
let i = 0, flag = false;
function tick() {
  i++;
  if (i % 1000 === 0) {
    console.log(`RSS [${i}]: ${process.memoryUsage().rss / 1024 / 1024} MiB`);
  }
  retained.push(Buffer.allocUnsafe(bs));
  if (i === 5000) {
    console.log('Clearing retained and enabling alloc');
    retained.length = 0;
    flag = true;
  }
  if (flag) Buffer.alloc(bs); // Buffer.alloc(bs - 10) seems to be fine here
  if (i < 10000) setImmediate(tick);
}
tick();

Atm (with Node.js v10.7.0) it produces the following results:

RSS [1000]: 35.140625 MiB
RSS [2000]: 40.24609375 MiB
RSS [3000]: 45.25390625 MiB
RSS [4000]: 49.27734375 MiB
RSS [5000]: 53.29296875 MiB
Clearing retained and enabling alloc
RSS [6000]: 993.45703125 MiB
RSS [7000]: 2233.32421875 MiB
RSS [8000]: 3499.56640625 MiB
RSS [9000]: 4792.9140625 MiB
RSS [10000]: 5997.30859375 MiB

I traced that down to C++ malloc() behavior (testcase in #21967 (comment)).

This is what happens just with LD_PRELOAD=/usr/lib/libjemalloc.so:

RSS [1000]: 36.640625 MiB
RSS [2000]: 42.62109375 MiB
RSS [3000]: 48.21875 MiB
RSS [4000]: 52.38671875 MiB
RSS [5000]: 56.7890625 MiB
Clearing retained and enabling alloc
RSS [6000]: 58.8828125 MiB
RSS [7000]: 62.77734375 MiB
RSS [8000]: 66.9140625 MiB
RSS [9000]: 71.2421875 MiB
RSS [10000]: 75.453125 MiB

Testcase 2:

const arr = [];
for (let i = 0; i < 1e4; i++) arr.push(Buffer.alloc(1e5));
console.log(`RSS: ${process.memoryUsage().rss / 1024 / 1024} MiB`);

Normal โ€” 697 MiB, jemalloc โ€” 37 MiB.

Testcase 3 (jemalloc consumes more memory):

function foo() {
  let a;
  for (let i = 0; i < 5; i++) a = Buffer.alloc(1e8, 1);
  console.log(`RSS: ${process.memoryUsage().rss / 1024 / 1024} MiB`);
}
const start = process.hrtime();
foo(); foo();
gc(); gc();
console.log(`RSS: ${process.memoryUsage().rss / 1024 / 1024} MiB`);
foo();
const time = process.hrtime(start);
console.log('Time:', time[0] + time[1] * 1e-9);

Normal:

RSS: 506.8125 MiB
RSS: 507.98046875 MiB
RSS: 31.2421875 MiB
RSS: 507.8828125 MiB
Time: 0.774285919

jemalloc:

RSS: 507.87890625 MiB
RSS: 604.40625 MiB
RSS: 604.625 MiB
RSS: 604.625 MiB
Time: 0.349531212

Testcase 4:

'use strict';
let i = 0;
function tick() {
  const a = Buffer.alloc(1e7);
  if (i++ >= 1e4) return;
  setImmediate(tick);
}
tick();

Measured with /usr/bin/time -f '%M KiB, %e seconds' node testcase-4.js.
Normal (1e4 * 1e7): 129 576 KiB, 11.41 seconds.
jemalloc (1e4 * 1e7): 34 928 KiB, 3.33 seconds.
Normal (1e5 * 1e6): 109 196 KiB, 11.51 seconds.
jemalloc (1e5 * 1e6): 35 636 KiB, 4.13 seconds.

Testcase 5 (like 4, but now we fill the buffer with 1-s):

'use strict';
let i = 0;
function tick() {
  const a = Buffer.alloc(1e7, 1);
  if (i++ >= 1e4) return;
  setImmediate(tick);
}
tick();

Normal (1e4 * 1e7): 139 060 KiB, 14.38 seconds.
jemalloc (1e4 * 1e7): 170 308 KiB, 10.65 seconds.
Normal (1e5 * 1e6): 105 120 KiB, 12.25 seconds.
jemalloc (1e5 * 1e6): 112 548 KiB, 10.99 seconds.

Testcase 6 (from #8871, where @bnoordhuis mentioned jemalloc):

const zlib = require('zlib');
const payload = Buffer.from(JSON.stringify({ some:"data" }));
for (let i =0; i < 30000; ++i) zlib.deflate(payload, () => {});

No improvement in this case and a 5% loss.
Normal: 3 022 496 KiB, 5.88 seconds.
jemalloc: 3 179 716 KiB, 5.91 seconds.

/cc @addaleax @bnoordhuis @mscdex

Read the original on github.com โ†—