Skip to content

Commit 6b25cfb

Browse files
authored
Add lld benchmarking script.
The purpose of this script is to measure the performance effect of an lld change in a statistically sound way, automating all the tedious parts of doing so. It copies the test case into /tmp as well as running the test binaries from /tmp to reduce the influence on the test machine's storage medium on the results. It accounts for measurement bias caused by binary layout (using the --randomize-section-padding flag to link the test binaries) and by environment variable size (implemented by hyperfine [1]). Runs of the base and test case are interleaved to account for environmental factors which may influence the result due to the passage of time. The results of running hyperfine are collected into a results.csv file in the output directory and may be analyzed by the user with a tool such as ministat. Requirements: Linux host, hyperfine [2] in $PATH, run from a build directory configured to use ninja and a recent version of lld that supports --randomize-section-padding, /tmp is tmpfs. [1] https://github.com/sharkdp/hyperfine/blob/3cedcc38d0c430cbf38b4364b441c43a938d2bf3/src/util/randomized_environment_offset.rs#L1 [2] https://github.com/sharkdp/hyperfine Reviewers: rnk, MaskRay, smithp35 Reviewed By: rnk Pull Request: llvm#138367
1 parent e882590 commit 6b25cfb

File tree

1 file changed

+151
-0
lines changed

1 file changed

+151
-0
lines changed

lld/utils/run_benchmark.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
# See https://llvm.org/LICENSE.txt for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# ==------------------------------------------------------------------------==#
8+
9+
import argparse
10+
import os
11+
import shutil
12+
import subprocess
13+
import tempfile
14+
15+
# The purpose of this script is to measure the performance effect
16+
# of an lld change in a statistically sound way, automating all the
17+
# tedious parts of doing so. It copies the test case into /tmp as well as
18+
# running the test binaries from /tmp to reduce the influence on the test
19+
# machine's storage medium on the results. It accounts for measurement
20+
# bias caused by binary layout (using the --randomize-section-padding
21+
# flag to link the test binaries) and by environment variable size
22+
# (implemented by hyperfine [1]). Runs of the base and test case are
23+
# interleaved to account for environmental factors which may influence
24+
# the result due to the passage of time. The results of running hyperfine
25+
# are collected into a results.csv file in the output directory and may
26+
# be analyzed by the user with a tool such as ministat.
27+
#
28+
# Requirements: Linux host, hyperfine [2] in $PATH, run from a build directory
29+
# configured to use ninja and a recent version of lld that supports
30+
# --randomize-section-padding, /tmp is tmpfs.
31+
#
32+
# [1] https://github.com/sharkdp/hyperfine/blob/3cedcc38d0c430cbf38b4364b441c43a938d2bf3/src/util/randomized_environment_offset.rs#L1
33+
# [2] https://github.com/sharkdp/hyperfine
34+
#
35+
# Example invocation for comparing the performance of the current commit
36+
# against the previous commit which is treated as the baseline, without
37+
# linking debug info:
38+
#
39+
# lld/utils/run_benchmark.py \
40+
# --base-commit HEAD^ \
41+
# --test-commit HEAD \
42+
# --test-case lld/utils/speed-test-reproducers/result/firefox-x64/response.txt \
43+
# --num-iterations 512 \
44+
# --num-binary-variants 16 \
45+
# --output-dir outdir \
46+
# --ldflags=-S
47+
#
48+
# Then this bash command will compare the real time of the base and test cases.
49+
#
50+
# ministat -A \
51+
# <(grep lld-base outdir/results.csv | cut -d, -f2) \
52+
# <(grep lld-test outdir/results.csv | cut -d, -f2)
53+
54+
# We don't want to copy stat() information when we copy the reproducer
55+
# to the temporary directory. Files in the Nix store are read-only so this will
56+
# cause trouble when the linker writes the output file and when we want to clean
57+
# up the temporary directory. Python doesn't provide a way to disable copying
58+
# stat() information in shutil.copytree so we just monkeypatch shutil.copystat
59+
# to do nothing.
60+
shutil.copystat = lambda *args, **kwargs: 0
61+
62+
parser = argparse.ArgumentParser(prog="benchmark_change.py")
63+
parser.add_argument("--base-commit", required=True)
64+
parser.add_argument("--test-commit", required=True)
65+
parser.add_argument("--test-case", required=True)
66+
parser.add_argument("--num-iterations", type=int, required=True)
67+
parser.add_argument("--num-binary-variants", type=int, required=True)
68+
parser.add_argument("--output-dir", required=True)
69+
parser.add_argument("--ldflags", required=False)
70+
args = parser.parse_args()
71+
72+
test_dir = tempfile.mkdtemp()
73+
print(f"Using {test_dir} as temporary directory")
74+
75+
os.makedirs(args.output_dir)
76+
print(f"Using {args.output_dir} as output directory")
77+
78+
79+
def extract_link_command(target):
80+
# We assume that the last command printed by "ninja -t commands" containing a
81+
# "-o" flag is the link command (we need to check for -o because subsequent
82+
# commands create symlinks for ld.lld and so on). This is true for CMake and
83+
# gn.
84+
link_command = None
85+
for line in subprocess.Popen(
86+
["ninja", "-t", "commands", target], stdout=subprocess.PIPE
87+
).stdout.readlines():
88+
commands = line.decode("utf-8").split("&&")
89+
for command in commands:
90+
if " -o " in command:
91+
link_command = command.strip()
92+
return link_command
93+
94+
95+
def generate_binary_variants(case_name):
96+
subprocess.run(["ninja", "lld"])
97+
link_command = extract_link_command("lld")
98+
99+
for i in range(0, args.num_binary_variants):
100+
print(f"Generating binary variant {i} for {case_name} case")
101+
command = f"{link_command} -o {test_dir}/lld-{case_name}{i} -Wl,--randomize-section-padding={i}"
102+
subprocess.run(command, check=True, shell=True)
103+
104+
105+
# Make sure that there are no local changes.
106+
subprocess.run(["git", "diff", "--exit-code", "HEAD"], check=True)
107+
108+
# Resolve the base and test commit, since if they are relative to HEAD we will
109+
# check out the wrong commit below.
110+
resolved_base_commit = subprocess.check_output(
111+
["git", "rev-parse", args.base_commit]
112+
).strip()
113+
resolved_test_commit = subprocess.check_output(
114+
["git", "rev-parse", args.test_commit]
115+
).strip()
116+
117+
test_case_dir = os.path.dirname(args.test_case)
118+
test_case_respfile = os.path.basename(args.test_case)
119+
120+
test_dir_test_case_dir = f"{test_dir}/testcase"
121+
shutil.copytree(test_case_dir, test_dir_test_case_dir)
122+
123+
subprocess.run(["git", "checkout", resolved_base_commit], check=True)
124+
generate_binary_variants("base")
125+
126+
subprocess.run(["git", "checkout", resolved_test_commit], check=True)
127+
generate_binary_variants("test")
128+
129+
130+
def hyperfine_link_command(case_name):
131+
return f'../lld-{case_name}$(({{iter}}%{args.num_binary_variants})) -flavor ld.lld @{test_case_respfile} {args.ldflags or ""}'
132+
133+
134+
results_csv = f"{args.output_dir}/results.csv"
135+
subprocess.run(
136+
[
137+
"hyperfine",
138+
"--export-csv",
139+
os.path.abspath(results_csv),
140+
"-P",
141+
"iter",
142+
"0",
143+
str(args.num_iterations - 1),
144+
hyperfine_link_command("base"),
145+
hyperfine_link_command("test"),
146+
],
147+
check=True,
148+
cwd=test_dir_test_case_dir,
149+
)
150+
151+
shutil.rmtree(test_dir)

0 commit comments

Comments
 (0)