Node.js buffer garbage collection - node.js

buf.slice([start[, end]])
Returns a new Buffer that references the same memory as the original, but offset and cropped by the start and end indices.
Note that modifying the new Buffer slice will modify the memory in the original Buffer because the allocated memory of the two objects overlap.
How does garbage collector handle allocated memory if one of the references is gone?

When you perform a slice on a Buffer, you are only creating a new reference to the original buffer, which starts and ends at different points.
If you change the original buffer, the sliced reference will also change.
What this means is that the entire chunk of memory won't be available for garbage collection until all references (sliced or not) are gone.
Hope this answers your question.

From the Node.js buffer documentation: "The implementation of Buffer#slice() creates a view over the existing Buffer without copying, making Buffer#slice() far more efficient.". This means that the buffers reference the same memory location resulting in the overlap. Only once all references to the buffer have been removed, can the memory be reallocated by the garbage collector (gc). When the gc runs, it will delete the buffers that have no references and return the memory to the respective pools.
Node buffers vary in behavior depending on how you initialized them. If you used the new Buffer() methods, they are now deprecated and should revisit the docs. You should use the buffer alloc(), bufferUnsafe() and bufferUnsafeSlow() methods.

Related

--max-semi-space-size vs --max-old-space-size

What is the the difference between
--max-old-space-size vs --max-semi-space-size in the Nodejs docs?
I understand that they are related to V8 garbage collector, but there doesn't seem to be any clear explanation within the docs.
Garbage collection is the process of reclaiming the memory occupied by
objects that are no longer in use by the application. Usually, memory
allocation is cheap while it’s expensive to collect when the memory
pool is exhausted.
An object is a candidate for garbage collection when it is unreachable
from the root node, so not referenced by the root object or any other
active objects. Root objects can be global objects, DOM elements or
local variables.
The heap has two main segments, the New Space and the Old
Space. The New Space is where new allocations are happening; it
is fast to collect garbage here and has a size of ~1-8MBs. Objects
living in the New Space are called Young Generation. The Old Space
where the objects that survived the collector in the New Space are
promoted into – they are called the Old Generation. Allocation in the
Old Space is fast, however collection is expensive so it is
infrequently performed .
--max-old-space-size is related to the Old Space limit whereas max-semi-space-size is related to the New Space.
New Space has a smaller buffer (as the GC runs more frequently) whereas Old Space is larger and runs less often.
source: risingstack garbage collection.
source: hunting a ghost

What's the difference between Buffer.allocUnsafe() and Buffer.allocUnsafeSlow() in NodeJS?

I've read the docs on the subject.
It says:
When using Buffer.allocUnsafe() to allocate new Buffer instances,
allocations under 4KB are sliced from a single pre-allocated Buffer.
This allows applications to avoid the garbage collection overhead of
creating many individually allocated Buffer instances. This approach
improves both performance and memory usage by eliminating the need to
track and clean up as many individual ArrayBuffer objects.
However, in the case where a developer may need to retain a small
chunk of memory from a pool for an indeterminate amount of time, it
may be appropriate to create an un-pooled Buffer instance using
Buffer.allocUnsafeSlow() and then copying out the relevant bits.
Also I've found this explanation:
Buffer.allocUnsafeSlow is different from Buffer.allocUnsafe() method. In
allocUnsafe() method, if buffer size is less than 4KB than it
automatically cut out the required buffer from a pre-allocated buffer
i.e. it does not initialize a new buffer. It saves memory by not
allocating many small Buffer instances. But if developer need to hold
on some amount of overhead memory for intermediate amount of time,
than allocUnsafeSlow() method can be used.
Though it is hard to understand for me. May you explain it more eloquent and detailed with some examples for both, please?

What is uninitialized memory and why isn't it initialized when allocating?

Taking this signature for a method of the GlobalAllocator:
unsafe fn alloc(&self, layout: Layout) -> *mut u8
and this sentence from the method's documentation:
The allocated block of memory may or may not be initialized.
Suppose that we are going to allocate some chunk of memory for an [i32, 10]. Assuming the size of i32 it's 4 bytes, our example array would need 40 bytes for the requested storage.
Now, the allocator found a memory spot that fits our requirements. Some 40 bytes of a memory region... but... what's there? I always read the term garbage data, and assume that it's just old data already stored there by another process, program... etc.
What's unitialized memory? Just data that is not initialized with zeros of with some default value for the type that we want to store there?
Why not always memory it's initialized before returning the pointer? It's too costly? But the memory must be initialized in order to use it properly and not cause UB. Why then doesn't comes already initialized?
When some resource it's deallocated, things musn't be pointing to that freed memory. That's that place got zeroed? What really happens when you deallocate some piece of memory?
What's unitialized memory? Just data that is not initialized with zeros of with some default value for the type that we want to store there?
It's worse than either of those. Reading from uninitialized memory is undefined behavior, as in you can no longer reason about a program which does so. Practically, compilers often optimize assuming that code paths that would trigger undefined behavior are never executed and their code can be removed. Or not, depending on how aggressive the compiler is.
If you could reliably read from the pointer, it would contain arbitrary data. It may be zeroes, it may be old data structures, it may be parts of old data structures. It may even be things like passwords and encryption keys, which is another reason why reading uninitialized memory is problematic.
Why not always memory it's initialized before returning the pointer? It's too costly? But the memory must be initialized in order to use it properly and not cause UB. Why then doesn't comes already initialized?
Yes, cost is the issue. The first thing that is typically done after allocating a piece of memory is to write to it. Having the allocator "pre-initialize" the memory is wasteful when the caller is going to overwrite it anyway with the values it wants. This is especially significant with large buffers used for IO or other large storage.
When some resource it's deallocated, things musn't be pointing to that freed memory. That's that place got zeroed? What really happens when you deallocate some piece of memory?
It's up to how the memory allocator is implemented. Most don't waste processing power to clear the data that's been deallocated, since it will be overwritten anyway when it's reallocated. Some allocators may write some bookkeeping data to the freed space. GlobalAllocator is an interface to whatever allocator the system comes with, so it can vary depending on the environment.
I always read the term garbage data, and assume that it's just old data already stored there by another process, program... etc.
Worth noting: all modern desktop OSs have memory isolation between processes - your program cannot access the memory of other processes or the kernel (unless you explicitly share it via specialized functionality). The kernel will clear memory before it assigns it to your process, to prevent leaking sensitive data. But you can see old data from your own process, for the reasons described above.
What you are asking are implementation details that can even vary from run to run. From the perspective of the abstract machine and thus the optimizer they don't matter.
Turning contents of uninitialized memory into almost any type (other than MaybeUninit) is immediate undefined behavior.
let mem: *u8 = unsafe { alloc(...) };
let x: u8 = unsafe { ptr::read(mem) };
if x != x {
print!("wtf");
}
May or may not print, crash or delete the contents of your harddrive, possibly even before reaching that alloc call because the optimizer worked backwards and eliminated the entire code block because it could prove that all execution paths are UB.
This may happen due to assumptions the optimizer relies on, i.e. even when the underlying allocator is well-behaved. But real systems may also behave non-deterministically. E.g. theoretically on a freshly booted embedded system memory might be in an uninitialized state that doesn't reliably return 0 or 1. Or on linux madvise(MADV_FREE) can cause allocations to return inconsistent results over time until initialized.

External memory vs. heap memory in node.js - any tradeoffs one way or the other?

In node.js, what is the difference between memory allocated in the node.js heap vs. memory reported as "external" as seen in process.memoryUsage()? As there advantages or disadvantages to one over the other? If you were going to have a particularly large object (gigabytes), does it matter which one is used? That doc only says this:
external refers to the memory usage of C++ objects bound to JavaScript
objects managed by V8.
It doesn't tell you what any consequences or tradeoffs might be of doing allocations that use this "external" memory instead of the heap memory.
I'm examining tradeoffs between using a large Array (regular Javascript Array object) vs. a large Uint32Array object. Apparently, an Array is always allocated in the heap, but a Uint32Array is always allocated from "external" memory (not in the heap).
Here the background. I have an object that needs a fairly large array of 32 bit integers as part of what it does. This array can be as much as a billion units long. I realized that I could cut the storage occupied by the array in half if I switched to a Uint32Array instead of a regular Array because a regular Array stores things as double precision floats (64 bits long) vs. a Uint32Array stores things as 32 bit values.
In a test jig, I measured that the Uint32Array does indeed use 1/2 the total storage for the same number of units long (32-bits per unit instead of 64-bits per unit). But, when looking at the results of process.memoryUsage(), I noticed that the Uint32Array storage is in the "external" bucket of storage whereas the array is in the "heapUsed" bucket of storage.
To add to the context a bit more, because a Uint32Array is not resizable, when I do need to change its size, I have to allocate a new Uint32Array and then copy data over from the original (which I suppose could lead to more memory fragmentation opportunites). With an Array, you can just change the size and the JS engine takes care of whatever has to happen internally to adjust to the new size.
What are the advantages/disadvantages of making a large storage allocation that's "external" vs. in the "heap"? Any reason to care?

Does the GHC garbage collector have any special optimisations for large objects?

Does the GHC garbage collector handle "large" objects specially? Or does it treat them exactly the same as any other object?
Some GC engines put large objects in a separate area, which gets scanned less regularly and possibly has a different collection algorithm (e.g., compacting instead of copying, or maybe even using freelists rather than attempting to defragment). Does GHC do anything like this?
Yes. The GHC heap is not kept in one contiguous stretch of memory; rather, it is organized into blocks.
When an allocated object’s size is above a specific threshold (block_size*8/10, where block_size is 4k, so roughly 3.2k), the block holding the object is marked as large (BF_LARGE). Now, when garbage collection occurs, rather than copy large objects from this block to a new one, the block itself is added to the new generation's set of blocks; this involves fiddling with a linked list (a large object list, to be precise).
Since this means that it may take a while for us to reclaim dead space inside a large block, it does mean that large objects can suffer from fragmentation, as seen in bug 7831. However, this doesn't usually occur until individual allocations hit half of the megablock size, 1M.

Resources