Skip to content

Commit 93472e3

Browse files
authored
Optimize llvm-nm (#13465)
* Optimize llvm-nm invocations from Emscripten. In python creating a multiprocessing pool is *extremely* slow, and can take 500ms-1 second. If there are only few files to llvm-nm, do those sequentially since a single llvm-nm only takes about 10-20ms. * Further optimize llvm-nm to avoid the multiprocessing pool completely. * Further optimize multiple llvm-nm calls by just invoking llvm-nm once: it accepts a list of files to process. * Flake * Flake * Handle llvm-nm nonzero exit code. * Allow calling llvm_nm_multiple with zero files
1 parent d492df4 commit 93472e3

File tree

2 files changed

+76
-48
lines changed

2 files changed

+76
-48
lines changed

tools/building.py

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,6 @@ def extract_archive_contents(archive_file):
118118
}
119119

120120

121-
# Due to a python pickling issue, the following two functions must be at top
122-
# level, or multiprocessing pool spawn won't find them.
123-
def g_llvm_nm_uncached(filename):
124-
return llvm_nm_uncached(filename)
125-
126-
127121
def g_multiprocessing_initializer(*args):
128122
for item in args:
129123
(key, value) = item.split('=', 1)
@@ -364,20 +358,82 @@ def make_paths_absolute(f):
364358
return os.path.abspath(f)
365359

366360

367-
# Runs llvm-nm in parallel for the given list of files.
361+
# Runs llvm-nm for the given list of files.
368362
# The results are populated in nm_cache
369-
# multiprocessing_pool: An existing multiprocessing pool to reuse for the operation, or None
370-
# to have the function allocate its own.
371-
def parallel_llvm_nm(files):
372-
with ToolchainProfiler.profile_block('parallel_llvm_nm'):
373-
pool = get_multiprocessing_pool()
374-
object_contents = pool.map(g_llvm_nm_uncached, files)
363+
def llvm_nm_multiple(files):
364+
with ToolchainProfiler.profile_block('llvm_nm_multiple'):
365+
if len(files) == 0:
366+
return []
367+
# Run llvm-nm on files that we haven't cached yet
368+
llvm_nm_files = [f for f in files if f not in nm_cache]
369+
370+
# We can issue multiple files in a single llvm-nm calls, but only if those
371+
# files are all .o or .bc files. Because of llvm-nm output format, we cannot
372+
# llvm-nm multiple .a files in one call, but those must be individually checked.
373+
if len(llvm_nm_files) > 1:
374+
llvm_nm_files = [f for f in files if f.endswith('.o') or f.endswith('.bc')]
375+
376+
if len(llvm_nm_files) > 0:
377+
cmd = [LLVM_NM] + llvm_nm_files
378+
results = run_process(cmd, stdout=PIPE, stderr=PIPE, check=False)
379+
380+
# If one or more of the input files cannot be processed, llvm-nm will return a non-zero error code, but it will still process and print
381+
# out all the other files in order. So even if process return code is non zero, we should always look at what we got to stdout.
382+
if results.returncode != 0:
383+
logger.debug('Subcommand ' + ' '.join(cmd) + ' failed with return code ' + str(results.returncode) + '! (An input file was corrupt?)')
384+
385+
results = results.stdout
386+
387+
# llvm-nm produces a single listing of form
388+
# file1.o:
389+
# 00000001 T __original_main
390+
# U __stack_pointer
391+
#
392+
# file2.o:
393+
# 0000005d T main
394+
# U printf
395+
#
396+
# ...
397+
# so loop over the report to extract the results
398+
# for each individual file.
399+
400+
filename = llvm_nm_files[0]
401+
402+
# When we dispatched more than one file, we must manually parse
403+
# the file result delimiters (like shown structured above)
404+
if len(llvm_nm_files) > 1:
405+
file_start = 0
406+
i = 0
407+
408+
while True:
409+
nl = results.find('\n', i)
410+
if nl < 0:
411+
break
412+
colon = results.rfind(':', i, nl)
413+
if colon >= 0 and results[colon + 1] == '\n': # New file start?
414+
nm_cache[filename] = parse_symbols(results[file_start:i - 1])
415+
filename = results[i:colon].strip()
416+
file_start = colon + 2
417+
i = nl + 1
418+
419+
nm_cache[filename] = parse_symbols(results[file_start:])
420+
else:
421+
# We only dispatched a single file, we can just parse that directly
422+
# to the output.
423+
nm_cache[filename] = parse_symbols(results)
424+
425+
# Any .a files that have multiple .o files will have hard time parsing. Scan those
426+
# sequentially to confirm. TODO: Move this to use run_multiple_processes()
427+
# when available.
428+
for f in files:
429+
if f not in nm_cache:
430+
nm_cache[f] = llvm_nm(f)
431+
432+
return [nm_cache[f] for f in files]
433+
375434

376-
for i, file in enumerate(files):
377-
if object_contents[i].returncode != 0:
378-
logger.debug('llvm-nm failed on file ' + file + ': return code ' + str(object_contents[i].returncode) + ', error: ' + object_contents[i].output)
379-
nm_cache[file] = object_contents[i]
380-
return object_contents
435+
def llvm_nm(file):
436+
return llvm_nm_multiple([file])[0]
381437

382438

383439
def read_link_inputs(files):
@@ -417,7 +473,7 @@ def clean_at_exit():
417473

418474
# Next, extract symbols from all object files (either standalone or inside archives we just extracted)
419475
# The results are not used here directly, but populated to llvm-nm cache structure.
420-
parallel_llvm_nm(object_names)
476+
llvm_nm_multiple(object_names)
421477

422478

423479
def llvm_backend_args():
@@ -754,34 +810,6 @@ def parse_symbols(output):
754810
return ObjectFileInfo(0, None, set(defs), set(undefs), set(commons))
755811

756812

757-
def llvm_nm_uncached(filename, stdout=PIPE, stderr=PIPE):
758-
# LLVM binary ==> list of symbols
759-
proc = run_process([LLVM_NM, filename], stdout=stdout, stderr=stderr, check=False)
760-
if proc.returncode == 0:
761-
return parse_symbols(proc.stdout)
762-
else:
763-
return ObjectFileInfo(proc.returncode, str(proc.stdout) + str(proc.stderr))
764-
765-
766-
def llvm_nm(filename, stdout=PIPE, stderr=PIPE):
767-
# Always use absolute paths to maximize cache usage
768-
filename = os.path.abspath(filename)
769-
770-
if filename in nm_cache:
771-
return nm_cache[filename]
772-
773-
ret = llvm_nm_uncached(filename, stdout, stderr)
774-
775-
if ret.returncode != 0:
776-
logger.debug('llvm-nm failed on file ' + filename + ': return code ' + str(ret.returncode) + ', error: ' + ret.output)
777-
778-
# Even if we fail, write the results to the NM cache so that we don't keep trying to llvm-nm the
779-
# failing file again later.
780-
nm_cache[filename] = ret
781-
782-
return ret
783-
784-
785813
def emcc(filename, args=[], output_filename=None, stdout=None, stderr=None, env=None):
786814
if output_filename is None:
787815
output_filename = filename + '.o'

tools/system_libs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1394,7 +1394,7 @@ def add_reverse_deps(need):
13941394
add_reverse_deps(need) # recurse to get deps of deps
13951395

13961396
# Scan symbols
1397-
symbolses = building.parallel_llvm_nm([os.path.abspath(t) for t in input_files])
1397+
symbolses = building.llvm_nm_multiple([os.path.abspath(t) for t in input_files])
13981398

13991399
warn_on_unexported_main(symbolses)
14001400

0 commit comments

Comments
 (0)