diff --git a/ChangeLog.md b/ChangeLog.md index c2e9b8ac929fb..9eb327949642c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,10 @@ See docs/process.md for more on how version tagging works. 6.0.14 (in development) ----------------------- +- In `MAIN_MODULE` builds the GOT entries (and table slots) for the main + module's exports are now created on demand, when first needed by a side + module, rather than all at startup. This can significantly reduce startup + time for programs with a large number of exports. (#27993) - When compiling and linking in a single command, auxiliary compiler output files (such as `.dwo` files from `-gsplit-dwarf` or `.json` files from `-ftime-trace`) are now named `-.`, matching the behavior diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index f4fe51f2fc3d9..59b5805afc2d8 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -159,9 +159,12 @@ var LibraryDylink = { // handler. We abuse the `target` of the Proxy in order to pass the set of // weak imports to the handler. $GOTHandler__internal: true, - $GOTHandler__deps: ['$GOT'], + $GOTHandler__deps: ['$GOT', '$addMainModuleGOTEntry'], $GOTHandler: { get(weakImports, symName) { + if (!GOT[symName]) { + addMainModuleGOTEntry(symName); + } var rtn = GOT[symName]; if (!rtn) { #if DYLINK_DEBUG == 2 @@ -210,8 +213,49 @@ var LibraryDylink = { ; }, + // The main module's (unrelocated) exports. Rather than creating GOT entries + // for all of these on startup, which is slow for programs with many exports + // since each exported function needs its own table slot, we create them on + // demand using `addMainModuleGOTEntry`. + $mainModuleExports: null, + + // Called once the main module has been instantiated. Only the GOT entries + // that the main module itself imports exist at this point, so these are the + // only ones we need to fill in. + $initMainModuleGOT__internal: true, + $initMainModuleGOT__deps: ['$GOT', '$mainModuleExports', '$addMainModuleGOTEntry'], + $initMainModuleGOT: (exports) => { + mainModuleExports = exports; + for (var [symName, entry] of Object.entries(GOT)) { + if (entry.value == {{{ UNDEFINED_ADDR }}}) { + addMainModuleGOTEntry(symName); + } + } + }, + + // Create (or fill in) the GOT entry for `symName` using the main module's + // definition, if it has one. This is called before any other definition of + // a symbol is added to the GOT, which gives the entry exactly the value it + // would have had if we had created GOT entries for all of the main module's + // exports on startup. In particular this means that the main module's + // definition of a symbol takes precedence over any side module definition. + $addMainModuleGOTEntry__internal: true, + $addMainModuleGOTEntry__deps: ['$mainModuleExports', '$updateGOT'], + $addMainModuleGOTEntry: (symName) => { +#if !WASM_BIGINT + // Prefer the version of the symbol without i64 legalization (see updateGOT). + if (mainModuleExports?.['orig$' + symName]) { + symName = 'orig$' + symName; + } +#endif + var value = mainModuleExports?.[symName]; + if (value) { + updateGOT({[symName]: value}, /*replace=*/true); + } + }, + $updateGOT__internal: true, - $updateGOT__deps: ['$GOT', '$isInternalSym', '$addFunction'], + $updateGOT__deps: ['$GOT', '$isInternalSym', '$addFunction', '$addMainModuleGOTEntry'], $updateGOT__docs: '/** @param {boolean=} replace */', $updateGOT: (exports, replace) => { #if DYLINK_DEBUG @@ -230,6 +274,11 @@ var LibraryDylink = { } #endif + if (!replace && !GOT[symName]) { + // The main module's definition, if any, takes precedence. + addMainModuleGOTEntry(symName); + } + var existingEntry = GOT[symName] && GOT[symName].value != {{{ UNDEFINED_ADDR }}}; if (replace || !existingEntry) { #if DYLINK_DEBUG == 2 diff --git a/src/preamble.js b/src/preamble.js index c6656a977b60f..170ff88550be2 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -798,7 +798,7 @@ function getWasmImports() { assignWasmExports(wasmExports); #if MAIN_MODULE - updateGOT(origExports); + initMainModuleGOT(origExports); #endif #if EXPORTED_RUNTIME_METHODS.has('wasmExports') diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index 15ca5b6de0401..eac1e1fc570ee 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26229, - "a.out.js.gz": 11137, + "a.out.js": 26379, + "a.out.js.gz": 11224, "a.out.nodebug.wasm": 16986, "a.out.nodebug.wasm.gz": 8667, - "total": 43215, - "total_gz": 19804, + "total": 43365, + "total_gz": 19891, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 4d4e9e1e31f10..76f20d6118a92 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 272704, + "a.out.js": 272855, "a.out.nodebug.wasm": 589443, - "total": 862147, + "total": 862298, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/test_core.py b/test/test_core.py index 7d7785b77b47c..c7b3f006c2e1f 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5261,6 +5261,107 @@ def test_dylink_weak(self): def test_dylink_weak_undef(self): self.dylink_testf(test_file('core/test_dylink_weak_undef.c')) + @needs_dylink + @parameterized({ + '': ([],), + 'pthreads': (['-pthread', '-sEXIT_RUNTIME', '-sPROXY_TO_PTHREAD', '-Wno-experimental'],), + }) + def test_dylink_main_module_precedence(self, args): + # When the main module and a side module both define a symbol, the main + # module's definition takes precedence when the symbol is accessed via the + # GOT, both from the defining side module (libside1) and from a later side + # module (libside2). The main module never takes the address of these + # symbols itself, so it has no GOT entries for them on startup. + if args: + self.require_pthreads() + self.cflags += args + create_file('libside1.c', r''' + int dup_data1 = 2; + int dup_func1(void) { return 2; } + int dup_data2 = 2; + int dup_func2(void) { return 2; } + void* side1_get_func(void) { return (void*)&dup_func1; } + int* side1_get_data(void) { return &dup_data1; } + ''') + create_file('libside2.c', r''' + int dup_func2(void); + extern int dup_data2; + void* side2_get_func(void) { return (void*)&dup_func2; } + int* side2_get_data(void) { return &dup_data2; } + ''') + create_file('libmissing.c', r''' + extern int missing_data; + int* missing_get_data(void) { return &missing_data; } + ''') + create_file('main.c', r''' + #include + #include + #include + #include + #ifdef __EMSCRIPTEN_PTHREADS__ + #include + #endif + + typedef int (*func_t)(void); + + EMSCRIPTEN_KEEPALIVE int dup_data1 = 1; + EMSCRIPTEN_KEEPALIVE int dup_func1(void) { return 1; } + EMSCRIPTEN_KEEPALIVE int dup_data2 = 1; + EMSCRIPTEN_KEEPALIVE int dup_func2(void) { return 1; } + + void* thread_main(void* arg) { + printf("thread: %d\n", ((func_t)arg)()); + return NULL; + } + + void check(const char* lib_name, const char* get_func, const char* get_data, + const char* func_name, const char* data_name) { + void* lib = dlopen(lib_name, RTLD_NOW); + assert(lib); + func_t f = ((func_t(*)(void))dlsym(lib, get_func))(); + int* d = ((int*(*)(void))dlsym(lib, get_data))(); + printf("%s: func=%d data=%d\n", lib_name, f(), *d); + assert(f == (func_t)dlsym(RTLD_DEFAULT, func_name)); + assert(d == (int*)dlsym(RTLD_DEFAULT, data_name)); + #ifdef __EMSCRIPTEN_PTHREADS__ + // Function pointers must be the same on a newly started thread. + pthread_t t; + pthread_create(&t, NULL, thread_main, f); + pthread_join(t, NULL); + #endif + } + + int main() { + check("libside1.so", "side1_get_func", "side1_get_data", "dup_func1", "dup_data1"); + check("libside2.so", "side2_get_func", "side2_get_data", "dup_func2", "dup_data2"); + printf("done\n"); + return 0; + } + ''') + create_file('missing.c', r''' + #include + #include + int main() { + printf("libmissing: %s\n", dlopen("libmissing.so", RTLD_NOW) ? "loaded" : "failed"); + return 0; + } + ''') + self.build_dlfcn_lib('libside1.c', outfile='libside1.so') + self.build_dlfcn_lib('libside2.c', outfile='libside2.so') + self.build_dlfcn_lib('libmissing.c', outfile='libmissing.so') + self.prep_dlfcn_main(libs=['libside1.so', 'libside2.so']) + expected = 'libside1.so: func=1 data=1\n' + if args: + expected += 'thread: 1\n' + expected += 'libside2.so: func=1 data=1\n' + if args: + expected += 'thread: 1\n' + expected += 'done\n' + self.do_runf('main.c', expected) + + # A missing (non-weak) GOT import is still an error. + self.do_runf('missing.c', 'libmissing: failed\n') + @needs_dylink def test_dylink_weak_multilib(self): create_file('liba.c', ''' diff --git a/tools/link.py b/tools/link.py index 062816b97464c..4daf5befb66d6 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1304,6 +1304,7 @@ def limit_incoming_module_api(): '$reportUndefinedSymbols', '$relocateExports', '$GOTHandler', + '$initMainModuleGOT', ] # shared modules need memory utilities to allocate their memory settings.ALLOW_TABLE_GROWTH = 1