Skip to content

Commit

Permalink
Initial new INITIAL_HEAP setting (#21071)
Browse files Browse the repository at this point in the history
Changes in default behavior:
1) INITIAL_HEAP is the new default for most builds.
   This means that there is an increase in the effective
   initial memory used by "sizeof(stack) + sizeof(static data)".
   In typical small applications this should be on the order
   of half a megabyte.
2) Because we cannot precisely calculate the amount
   of initial memory now, ASAN support will use
   the conservative upper estimate of MAXIMUM_MEMORY.
   This only affects ALLOW_MEMORY_GROWTH=0 builds.

This change does not yet enable INITIAL_HEAP for builds
that instantiate the memory in JS, e. g. with threading.
  • Loading branch information
SingleAccretion committed Mar 1, 2024
1 parent e8262f6 commit 22a1c04
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 47 deletions.
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ See docs/process.md for more on how version tagging works.
data into an external .mem file). This feature was only available under
wasm2js (`-sWASM=0`) anyway so this change will only affect users of this
setting. (#21217)
- `INITIAL_HEAP` setting is introduced to control the amount of initial
memory available for dynamic allocation without capping it. If you are
using `INITIAL_MEMORY`, consider switching to `INITIAL_HEAP`. Note that
it is currently not supported in all configurations (#21071).

3.1.54 - 02/15/24
-----------------
Expand Down
16 changes: 16 additions & 0 deletions site/source/docs/tools_reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,19 @@ Note that this setting does not affect the behavior of operator new in C++.
This function will always abort on allocation failure if exceptions are disabled.
If you want new to return 0 on failure, use it with std::nothrow.

.. _initial_heap:

INITIAL_HEAP
============

The initial amount of heap memory available to the program. This is the
memory region available for dynamic allocations via `sbrk`, `malloc` and `new`.

Unlike INITIAL_MEMORY, this setting allows the static and dynamic regions of
your programs memory to independently grow. In most cases we recommend using
this setting rather than `INITIAL_MEMORY`. However, this setting does not work
for imported memories (e.g. when dynamic linking is used).

.. _initial_memory:

INITIAL_MEMORY
Expand All @@ -162,6 +175,9 @@ we need to copy the old heap into a new one in that case.
If ALLOW_MEMORY_GROWTH is set, this initial amount of memory can increase
later; if not, then it is the final and total amount of memory.

By default, this value is calculated based on INITIAL_HEAP, STACK_SIZE,
as well the size of static data in input modules.

(This option was formerly called TOTAL_MEMORY.)

.. _maximum_memory:
Expand Down
1 change: 0 additions & 1 deletion src/postamble_minimal.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ WebAssembly.instantiate(Module['wasm'], imports).then((output) => {
wasmMemory = wasmExports['memory'];
#if ASSERTIONS
assert(wasmMemory);
assert(wasmMemory.buffer.byteLength === {{{ INITIAL_MEMORY }}});
#endif
updateMemoryViews();
#endif
Expand Down
4 changes: 0 additions & 4 deletions src/preamble.js
Original file line number Diff line number Diff line change
Expand Up @@ -966,10 +966,6 @@ function createWasm() {
{{{ receivedSymbol('wasmMemory') }}}
#if ASSERTIONS
assert(wasmMemory, 'memory not found in wasm exports');
// This assertion doesn't hold when emscripten is run in --post-link
// mode.
// TODO(sbc): Read INITIAL_MEMORY out of the wasm file in post-link mode.
//assert(wasmMemory.buffer.byteLength === {{{ INITIAL_MEMORY }}});
#endif
updateMemoryViews();
#endif
Expand Down
16 changes: 15 additions & 1 deletion src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,29 @@ var MALLOC = "dlmalloc";
// [link]
var ABORTING_MALLOC = true;

// The initial amount of heap memory available to the program. This is the
// memory region available for dynamic allocations via `sbrk`, `malloc` and `new`.
//
// Unlike INITIAL_MEMORY, this setting allows the static and dynamic regions of
// your programs memory to independently grow. In most cases we recommend using
// this setting rather than `INITIAL_MEMORY`. However, this setting does not work
// for imported memories (e.g. when dynamic linking is used).
//
// [link]
var INITIAL_HEAP = 16777216;

// The initial amount of memory to use. Using more memory than this will
// cause us to expand the heap, which can be costly with typed arrays:
// we need to copy the old heap into a new one in that case.
// If ALLOW_MEMORY_GROWTH is set, this initial amount of memory can increase
// later; if not, then it is the final and total amount of memory.
//
// By default, this value is calculated based on INITIAL_HEAP, STACK_SIZE,
// as well the size of static data in input modules.
//
// (This option was formerly called TOTAL_MEMORY.)
// [link]
var INITIAL_MEMORY = 16777216;
var INITIAL_MEMORY = -1;

// Set the maximum size of memory in the wasm module (in bytes). This is only
// relevant when ALLOW_MEMORY_GROWTH is set, as without growth, the size of
Expand Down
2 changes: 1 addition & 1 deletion test/code_size/hello_world_wasm2js.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function e(b) {
m[43] = 62;
m[47] = 63;
return function(c) {
var a = new ArrayBuffer(16777216), f = new Uint8Array(a), v = c.a.a;
var a = new ArrayBuffer(16908288), f = new Uint8Array(a), v = c.a.a;
q = f;
x(q, 1024, "aGVsbG8h");
c = u([]);
Expand Down
3 changes: 2 additions & 1 deletion test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2108,7 +2108,7 @@ def test_memorygrowth_geometric_step(self):
if self.is_wasm2js():
self.skipTest('wasm memory specific test')

self.emcc_args += ['-sALLOW_MEMORY_GROWTH', '-sMEMORY_GROWTH_GEOMETRIC_STEP=8.5', '-sMEMORY_GROWTH_GEOMETRIC_CAP=32MB']
self.emcc_args += ['-sINITIAL_MEMORY=16MB', '-sALLOW_MEMORY_GROWTH', '-sMEMORY_GROWTH_GEOMETRIC_STEP=8.5', '-sMEMORY_GROWTH_GEOMETRIC_CAP=32MB']
self.do_core_test('test_memorygrowth_geometric_step.c')

def test_memorygrowth_3_force_fail_reallocBuffer(self):
Expand Down Expand Up @@ -6006,6 +6006,7 @@ def test_unistd_sysconf_phys_pages(self):
assert self.get_setting('INITIAL_MEMORY') == '2200mb'
expected = (2200 * 1024 * 1024) // webassembly.WASM_PAGE_SIZE
else:
self.set_setting('INITIAL_MEMORY', '16mb')
expected = 16 * 1024 * 1024 // webassembly.WASM_PAGE_SIZE
self.do_runf(filename, str(expected) + ', errno: 0')

Expand Down
32 changes: 29 additions & 3 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -6560,7 +6560,7 @@ def test_massive_alloc(self, wasm):
return x == 0; // can't alloc it, but don't fail catastrophically, expect null
}
''')
cmd = [EMCC, 'main.c', '-sALLOW_MEMORY_GROWTH']
cmd = [EMCC, 'main.c', '-sALLOW_MEMORY_GROWTH', '-sINITIAL_MEMORY=16MB']
if not wasm:
cmd += ['-sWASM=0']
self.run_process(cmd)
Expand Down Expand Up @@ -6619,7 +6619,7 @@ def test_failing_alloc(self):
printf("managed another malloc!\n");
}
''' % (pre_fail, post_fail))
args = [EMXX, 'main.cpp', '-sEXPORTED_FUNCTIONS=_main,_sbrk'] + opts + aborting_args
args = [EMXX, 'main.cpp', '-sEXPORTED_FUNCTIONS=_main,_sbrk', '-sINITIAL_MEMORY=16MB'] + opts + aborting_args
args += ['-sTEST_MEMORY_GROWTH_FAILS'] # In this test, force memory growing to fail
if growth:
args += ['-sALLOW_MEMORY_GROWTH']
Expand Down Expand Up @@ -8154,6 +8154,29 @@ def test_binaryen_warn_mem(self):
self.run_process([EMCC, test_file('hello_world.c'), '-sINITIAL_MEMORY=' + str(16 * 1024 * 1024), '--pre-js', 'pre.js', '-sALLOW_MEMORY_GROWTH', '-sWASM_ASYNC_COMPILATION=0', '-sIMPORTED_MEMORY'])
self.assertContained('hello, world!', self.run_js('a.out.js'))

@parameterized({
'': ([], 16 * 1024 * 1024), # Default behavior: 16MB initial heap
'explicit': (['-sINITIAL_HEAP=64KB'], 64 * 1024), # Explicitly set initial heap is passed
'with_initial_memory': (['-sINITIAL_MEMORY=40MB'], 0), # Backwards compatibility: no initial heap (we can't tell if it'll fit)
'with_maximum_memory': (['-sMAXIMUM_MEMORY=40MB', '-sALLOW_MEMORY_GROWTH=1'], 0), # Backwards compatibility: no initial heap (we can't tell if it'll fit)
'with_all': (['-sINITIAL_HEAP=128KB', '-sINITIAL_MEMORY=20MB', '-sMAXIMUM_MEMORY=40MB', '-sALLOW_MEMORY_GROWTH=1'], 128 * 1024),
'limited_by_initial_memory': (['-sINITIAL_HEAP=10MB', '-sINITIAL_MEMORY=10MB'], None), # Not enough space for stack
'limited_by_maximum_memory': (['-sINITIAL_HEAP=5MB', '-sMAXIMUM_MEMORY=5MB', '-sALLOW_MEMORY_GROWTH=1'], None), # Not enough space for stack
})
def test_initial_heap(self, args, expected_initial_heap):
cmd = [EMCC, test_file('hello_world.c'), '-v'] + args

if expected_initial_heap is None:
out = self.expect_fail(cmd)
self.assertContained('wasm-ld: error:', out)
return

out = self.run_process(cmd, stderr=PIPE)
if expected_initial_heap != 0:
self.assertContained(f'--initial-heap={expected_initial_heap}', out.stderr)
else:
self.assertNotContained('--initial-heap=', out.stderr)

def test_memory_size(self):
for args, expect_initial, expect_max in [
([], 320, 320),
Expand Down Expand Up @@ -8183,6 +8206,9 @@ def test_invalid_mem(self):
self.assertContained('hello, world!', self.run_js('a.out.js'))

# Must be a multiple of 64KB
ret = self.expect_fail([EMCC, test_file('hello_world.c'), '-sINITIAL_HEAP=32505857', '-sALLOW_MEMORY_GROWTH']) # 31MB + 1 byte
self.assertContained('INITIAL_HEAP must be a multiple of WebAssembly page size (64KiB)', ret)

ret = self.expect_fail([EMCC, test_file('hello_world.c'), '-sINITIAL_MEMORY=33554433']) # 32MB + 1 byte
self.assertContained('INITIAL_MEMORY must be a multiple of WebAssembly page size (64KiB)', ret)

Expand Down Expand Up @@ -8634,7 +8660,7 @@ def run(args, expected):
result = self.run_js('a.out.js').strip()
self.assertEqual(result, f'{expected}, errno: 0')

run([], 256)
run([], 258)
run(['-sINITIAL_MEMORY=32MB'], 512)
run(['-sINITIAL_MEMORY=32MB', '-sALLOW_MEMORY_GROWTH'], (2 * 1024 * 1024 * 1024) // webassembly.WASM_PAGE_SIZE)
run(['-sINITIAL_MEMORY=32MB', '-sALLOW_MEMORY_GROWTH', '-sWASM=0'], (2 * 1024 * 1024 * 1024) // webassembly.WASM_PAGE_SIZE)
Expand Down
18 changes: 11 additions & 7 deletions tools/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,17 @@ def lld_flags_for_executable(external_symbols):
cmd.append('--growable-table')

if not settings.SIDE_MODULE:
# Export these two section start symbols so that we can extract the string
# data that they contain.
cmd += [
'-z', 'stack-size=%s' % settings.STACK_SIZE,
'--initial-memory=%d' % settings.INITIAL_MEMORY,
'--max-memory=%d' % settings.MAXIMUM_MEMORY,
]
cmd += ['-z', 'stack-size=%s' % settings.STACK_SIZE]

if settings.ALLOW_MEMORY_GROWTH:
cmd += ['--max-memory=%d' % settings.MAXIMUM_MEMORY]
else:
cmd += ['--no-growable-memory']

if settings.INITIAL_HEAP != -1:
cmd += ['--initial-heap=%d' % settings.INITIAL_HEAP]
if settings.INITIAL_MEMORY != -1:
cmd += ['--initial-memory=%d' % settings.INITIAL_MEMORY]

if settings.STANDALONE_WASM:
# when settings.EXPECT_MAIN is set we fall back to wasm-ld default of _start
Expand Down
117 changes: 88 additions & 29 deletions tools/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,25 +555,92 @@ def include_and_export(name):
settings.EXPORTED_RUNTIME_METHODS += ['ExitStatus']


def set_initial_memory():
user_specified_initial_heap = 'INITIAL_HEAP' in user_settings

# INITIAL_HEAP cannot be used when the memory object is created in JS: we don't know
# the size of static data here and thus the total initial memory size.
if settings.IMPORTED_MEMORY:
if user_specified_initial_heap:
# Some of these could (and should) be implemented.
exit_with_error('INITIAL_HEAP is currently not compatible with IMPORTED_MEMORY (which is enabled indirectly via SHARED_MEMORY, RELOCATABLE, ASYNCIFY_LAZY_LOAD_CODE)')
# The default for imported memory is to fall back to INITIAL_MEMORY.
settings.INITIAL_HEAP = -1

if not user_specified_initial_heap:
# For backwards compatibility, we will only use INITIAL_HEAP by default when the user
# specified neither INITIAL_MEMORY nor MAXIMUM_MEMORY. Both place an upper bounds on
# the overall initial linear memory (stack + static data + heap), and we do not know
# the size of static data at this stage. Setting any non-zero initial heap value in
# this scenario would risk pushing users over the limit they have set.
user_specified_initial = settings.INITIAL_MEMORY != -1
user_specified_maximum = 'MAXIMUM_MEMORY' in user_settings or 'WASM_MEM_MAX' in user_settings or 'BINARYEN_MEM_MAX' in user_settings
if user_specified_initial or user_specified_maximum:
settings.INITIAL_HEAP = -1

# Apply the default if we are going with INITIAL_MEMORY.
if settings.INITIAL_HEAP == -1 and settings.INITIAL_MEMORY == -1:
default_setting('INITIAL_MEMORY', 16 * 1024 * 1024)

def check_memory_setting(setting):
if settings[setting] % webassembly.WASM_PAGE_SIZE != 0:
exit_with_error(f'{setting} must be a multiple of WebAssembly page size (64KiB), was {settings[setting]}')
if settings[setting] >= 2**53:
exit_with_error(f'{setting} must be smaller than 2^53 bytes due to JS Numbers (doubles) being used to hold pointer addresses in JS side')

# Due to the aforementioned lack of knowledge about the static data size, we delegate
# checking the overall consistency of these settings to wasm-ld.
if settings.INITIAL_HEAP != -1:
check_memory_setting('INITIAL_HEAP')

if settings.INITIAL_MEMORY != -1:
check_memory_setting('INITIAL_MEMORY')
if settings.INITIAL_MEMORY < settings.STACK_SIZE:
exit_with_error(f'INITIAL_MEMORY must be larger than STACK_SIZE, was {settings.INITIAL_MEMORY} (STACK_SIZE={settings.STACK_SIZE})')

check_memory_setting('MAXIMUM_MEMORY')
if settings.MEMORY_GROWTH_LINEAR_STEP != -1:
check_memory_setting('MEMORY_GROWTH_LINEAR_STEP')


# Set an upper estimate of what MAXIMUM_MEMORY should be. Take note that this value
# may not be precise, and is only an upper bound of the exact value calculated later
# by the linker.
def set_max_memory():
# When memory growth is disallowed set MAXIMUM_MEMORY equal to INITIAL_MEMORY
# With INITIAL_HEAP, we only know the lower bound on initial memory size.
initial_memory_known = settings.INITIAL_MEMORY != -1

if not settings.ALLOW_MEMORY_GROWTH:
if 'MAXIMUM_MEMORY' in user_settings:
diagnostics.warning('unused-command-line-argument', 'MAXIMUM_MEMORY is only meaningful with ALLOW_MEMORY_GROWTH')
settings.MAXIMUM_MEMORY = settings.INITIAL_MEMORY
# Optimization: lower the default maximum memory to initial memory if possible.
if initial_memory_known:
settings.MAXIMUM_MEMORY = settings.INITIAL_MEMORY

# Automaticaly up the default maximum when the user requested a large minimum.
if 'MAXIMUM_MEMORY' not in user_settings:
if settings.ALLOW_MEMORY_GROWTH and settings.INITIAL_MEMORY > 2 * 1024 * 1024 * 1024:
if settings.ALLOW_MEMORY_GROWTH:
if any([settings.INITIAL_HEAP != -1 and settings.INITIAL_HEAP >= 2 * 1024 * 1024 * 1024,
initial_memory_known and settings.INITIAL_MEMORY > 2 * 1024 * 1024 * 1024]):
settings.MAXIMUM_MEMORY = 4 * 1024 * 1024 * 1024

# INITIAL_MEMORY sets a lower bound for MAXIMUM_MEMORY
if settings.INITIAL_MEMORY > settings.MAXIMUM_MEMORY:
if initial_memory_known and settings.INITIAL_MEMORY > settings.MAXIMUM_MEMORY:
settings.MAXIMUM_MEMORY = settings.INITIAL_MEMORY

if settings.MAXIMUM_MEMORY < settings.INITIAL_MEMORY:
# A similar check for INITIAL_HEAP would not be precise and so is delegated to wasm-ld.
if initial_memory_known and settings.MAXIMUM_MEMORY < settings.INITIAL_MEMORY:
exit_with_error('MAXIMUM_MEMORY cannot be less than INITIAL_MEMORY')


def inc_initial_memory(delta):
# Both INITIAL_HEAP and INITIAL_MEMORY can be set at the same time. Increment both.
if settings.INITIAL_HEAP != -1:
settings.INITIAL_HEAP += delta
if settings.INITIAL_MEMORY != -1:
settings.INITIAL_MEMORY += delta


def check_browser_versions():
# Map of setting all VM version settings to the minimum version
# we support.
Expand Down Expand Up @@ -1362,18 +1429,10 @@ def phase_linker_setup(options, state, newargs):
'removeRunDependency',
]

def check_memory_setting(setting):
if settings[setting] % webassembly.WASM_PAGE_SIZE != 0:
exit_with_error(f'{setting} must be a multiple of WebAssembly page size (64KiB), was {settings[setting]}')
if settings[setting] >= 2**53:
exit_with_error(f'{setting} must be smaller than 2^53 bytes due to JS Numbers (doubles) being used to hold pointer addresses in JS side')
if settings.SHARED_MEMORY or settings.RELOCATABLE or settings.ASYNCIFY_LAZY_LOAD_CODE:
settings.IMPORTED_MEMORY = 1

check_memory_setting('INITIAL_MEMORY')
check_memory_setting('MAXIMUM_MEMORY')
if settings.INITIAL_MEMORY < settings.STACK_SIZE:
exit_with_error(f'INITIAL_MEMORY must be larger than STACK_SIZE, was {settings.INITIAL_MEMORY} (STACK_SIZE={settings.STACK_SIZE})')
if settings.MEMORY_GROWTH_LINEAR_STEP != -1:
check_memory_setting('MEMORY_GROWTH_LINEAR_STEP')
set_initial_memory()

if settings.EXPORT_ES6:
if not settings.MODULARIZE:
Expand Down Expand Up @@ -1433,9 +1492,6 @@ def check_memory_setting(setting):
(options.shell_path == DEFAULT_SHELL_HTML or options.shell_path == utils.path_from_root('src/shell_minimal.html')):
exit_with_error(f'Due to collision in variable name "Module", the shell file "{options.shell_path}" is not compatible with build options "-sMODULARIZE -sEXPORT_NAME=Module". Either provide your own shell file, change the name of the export to something else to avoid the name collision. (see https://github.com/emscripten-core/emscripten/issues/7950 for details)')

if settings.SHARED_MEMORY or settings.RELOCATABLE or settings.ASYNCIFY_LAZY_LOAD_CODE:
settings.IMPORTED_MEMORY = 1

if settings.WASM_BIGINT:
settings.LEGALIZE_JS_FFI = 0

Expand Down Expand Up @@ -1496,9 +1552,9 @@ def check_memory_setting(setting):
# These values are designed be an over-estimate of the actual requirements and
# are based on experimentation with different tests/programs under asan and
# lsan.
settings.INITIAL_MEMORY += 50 * 1024 * 1024
inc_initial_memory(50 * 1024 * 1024)
if settings.PTHREADS:
settings.INITIAL_MEMORY += 50 * 1024 * 1024
inc_initial_memory(50 * 1024 * 1024)

if settings.USE_OFFSET_CONVERTER:
if settings.WASM2JS:
Expand Down Expand Up @@ -1541,22 +1597,23 @@ def check_memory_setting(setting):
if 'GLOBAL_BASE' in user_settings:
exit_with_error("ASan does not support custom GLOBAL_BASE")

# Increase the TOTAL_MEMORY and shift GLOBAL_BASE to account for
# Increase the INITIAL_MEMORY and shift GLOBAL_BASE to account for
# the ASan shadow region which starts at address zero.
# The shadow region is 1/8th the size of the total memory and is
# itself part of the total memory.
# We use the following variables in this calculation:
# - user_mem : memory usable/visible by the user program.
# - shadow_size : memory used by asan for shadow memory.
# - total_mem : the sum of the above. this is the size of the wasm memory (and must be aligned to WASM_PAGE_SIZE)
user_mem = settings.INITIAL_MEMORY
if settings.ALLOW_MEMORY_GROWTH:
user_mem = settings.MAXIMUM_MEMORY
user_mem = settings.MAXIMUM_MEMORY
if not settings.ALLOW_MEMORY_GROWTH and settings.INITIAL_MEMORY != -1:
user_mem = settings.INITIAL_MEMORY

# Given the know value of user memory size we can work backwards
# to find the total memory and the shadow size based on the fact
# that the user memory is 7/8ths of the total memory.
# (i.e. user_mem == total_mem * 7 / 8
# TODO-Bug?: this does not look to handle 4GB MAXIMUM_MEMORY correctly.
total_mem = user_mem * 8 / 7

# But we might need to re-align to wasm page size
Expand All @@ -1569,10 +1626,12 @@ def check_memory_setting(setting):
# We don't need to worry about alignment here. wasm-ld will take care of that.
settings.GLOBAL_BASE = shadow_size

if not settings.ALLOW_MEMORY_GROWTH:
settings.INITIAL_MEMORY = total_mem
else:
settings.INITIAL_MEMORY += align_to_wasm_page_boundary(shadow_size)
# Adjust INITIAL_MEMORY (if needed) to account for the shifted global base.
if settings.INITIAL_MEMORY != -1:
if settings.ALLOW_MEMORY_GROWTH:
settings.INITIAL_MEMORY += align_to_wasm_page_boundary(shadow_size)
else:
settings.INITIAL_MEMORY = total_mem

if settings.SAFE_HEAP:
# SAFE_HEAP instruments ASan's shadow memory accesses.
Expand Down

0 comments on commit 22a1c04

Please sign in to comment.