How Native Client handles memory
During compilation, the generated ELF image contains segments that have initialized data values and a list of constructors for global objects (C++ global constructors must run before main). The compiler and linker must lay global variables out in the address space.
On the first launch of a Native Client module, Chrome allocates 1 GB of contiguous memory (on both 32- and 64-bit architectures). The start location and offsets of this memory are noted for future use. Note that this reserved memory is shared between all running Native Client processes. Currently there is no API to detect the available memory from this heap. Native Client modules must be very sensitive to this sharing.
Each Native Client module gets its own virtual address space. 32-bit modules get 1 GB, and 64-bit modules get 4 GB. From the point of view of the Native Client module, the address space is private and zero-based. However, the Native Client module's execution environment is actually within the service runtime process, which handles the coarse-grained memory management, translating memory allocations to requests to the underlying operating system.
There is an important distinction to be made here: The virtual address space of 1 GB (or 4 GB) is reserved but not necessarily committed. At startup, the service runtime has to find a contiguous 1 GB (or 4 GB) region to reserve, but it doesn't actually require that there is enough physical (or swap) memory for all of it. When the application asks for more memory by making trampoline syscalls to the service runtime, Chrome then asks the underlying OS for committed memory. (Note that some OSes have an overcommit policy, since programs often ask for memory but don't actually use it – user-level memory allocators such as the malloc/new/free/delete library functions in newlib/glibc often ask for large chunks of memory to dole out to the user-level application code because system calls are expensive compared to library calls.) The committed memory is per Native Client process; there is no sharing of its contents unless the application explicitly asks for shared memory objects.
The only sensitivity that Native Client modules should have is to avoid over-consumption of memory resources. Depending on the underlying OS, over-consumption of memory could lead to run-time crashes (e.g., the Linux out-of-memory killer looks for processes to kill when overcommit problem occurs) or other failures (e.g., memory allocation syscall returns failure).
Upon allocation by the calling program, a memory request is sent down to either newlib or glibc, which handles allocation and management of the memory blocks from the reserved memory (memory that the service runtime has made available to the application library, i.e., the newlib or glibc allocators, but not yet made available to the higher level application code). No memory is distributed from outside the reserved block. Thread safety, fragmentation, etc. are all handled by the memory allocation library that is linked in.
There is no runtime memory access validation in Native Client other than:
- sandboxing of instructions that is ensured by the load-time static validator
- hardware exception generation, e.g., based on invalid memory accesses into guard memory pages or into reserved-but-not-committed memory pages when a bad pointer is used
Upon de-allocation by the calling program, a request is sent to either newlib or glibc, which handles de-allocation of the reserved memory.
Some allocators (e.g., glibc's tcmalloc) will both keep some memory in its free pool and return larger blocks of memory to the system. How memory blocks are returned depends on the allocator – some use sbrk, others undo mmap page mapping, etc. – but this is relatively infrequent: on A typical free/delete, memory is simply returned to a free memory pool.
When a Native Client module exits (usually as a side effect of the user closing a Chrome tab or surfing away to another page), all memory resources are returned to the underlying OS. No leaks or fragmentation should occur.
The function sizeof(void*) returns four bytes on all Native Client platforms, including 64-bit. For portability reasons it is important that sizeof(foo) returns the same answer on all Native Client platforms. For specifics as to why this architecture decision was made, see the Native Client publications.
Managing your memory for performance and stability
Memory management is a complex topic in computer programming, and can often be the cause of serious performance issues. Managing memory effeciently is not an issue specific to Native Client. The following reccomendations may help with your application's performance and stability:
- Use free-lists of objects where applicable.
- Understand the allocation scheme of your container classes, and use custom containers where applicable.
- Avoid dynamic memory allocation where it is not needed.
- Managing your own heaps may yield signficant performance improvements due to the allocation patterns and lack of alloc/dealloc overhead introduced by Native Client. TCMalloc, dlmalloc, and TLSF are some example options.