文章探讨如何让 PGO 优化实现可复现构建
来源标题:Making profile-guided optimization reproducible
Profile-guided optimization(PGO)能为程序带来明显加速,作者构建的 PGO 版 sqlite3 在特定负载下提速 1.32 倍。但 PGO 构建不可复现,导致 Python、GCC 等在 Nixpkgs 和 Guix 中默认关闭该优化。文章讨论了在包管理器中让 PGO 可复现的问题。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
Making profile-guided optimization reproducible
Profile-guided optimization (PGO) 是一种加快程序速度的简便方法,尤其当工作负载相对稳定时。在《把性能留在桌面上》一文中,我为 sqlite3 构建了一个 profile-guided 变体,使我们的 select 工作负载获得了 1.32 倍的加速。
编译器和解释器是 PGO 的绝佳候选对象,因为训练工作负载与实际工作负载看起来一样:编译代码本身就是在编译代码。它们从中获益也最大,因为它们支撑着如此多的其他程序;编译器或解释器上的一点加速,就可能对整体系统性能产生很大影响。
尽管有这些好处,PGO 在包管理器中并未被广泛使用。例如,Python 和 GCC 都支持 profile-guided 构建,但在 Nixpkgs(Python、GCC)和 Guix(Python)中被明确禁用。
旁注 Nix 和 Guix 都不需要可复现构建来保持一致。它们的存储路径是输入寻址的,即路径仅由输入推导而来。它们可以产生字节不同却声称同一路径的输出,而先到达二进制缓存的那一个就是所有人下载的那一个。
许多包管理器将可复现构建作为目标,而 PGO 是不可复现的。
当在独立基础设施上从相同源代码编译产生逐位相同的结果时,这让人确信构建基础设施没有被入侵,且产物确实对应于源代码。
要构建一个 profile-guided 程序,编译器首先用插桩构建它。在该构建上运行训练工作负载会收集 profile 数据,这就是“profile-guided”的部分。然后编译器在第二次构建中使用该 profile 来优化程序。^1 插桩后的程序会统计它走了哪些分支,第二次构建则带着这些计数重新编译程序。编译器用它们来决定内联什么、哪些块放在一起,以及哪些函数是热或冷的。问题在于训练运行是不可复现的,^2 因此 profile 在构建之间可能不同,优化后的二进制也随之不同。
不过我们确实想要 profile-guided 构建,因为它是加快程序速度的如此简便的方法。我们能让 PGO 可复现,从而既拥有蛋糕又吃掉它吗?
我一直在构建 Rewind,一个确定性虚拟机。程序的执行成为其输入的纯函数。我主要用它来调查和发现 bug^3,或作为使构建逐位可复现的一种方式。我想到,使 Rewind 对调试和可复现构建有用的那些相同属性,也可以使 PGO 可复现。
旁注: 刚接触 Rewind?从你只捕获一次的 flaky 构建和 dafuq 是一个确定性虚拟机开始。
这是一个程序,其 profile 取决于其线程运行的顺序。它把 100,000 个作业交给两个 worker:一个 worker 总是运行 checksum,另一个总是运行 compress,所以每个函数运行的频率取决于哪个 worker 接哪个作业。这取决于线程调度,而线程调度是非确定性的,所以 profile 也是非确定性的。
static void *worker(void *arg)
long id = (long)arg, acc = 0;
for (int job; (job = take()) != DONE;) {
/* The seed picks which worker checksums and which compresses. */
if ((id ^ seed) & 1) {
acc += checksum(job ^ seed);
} else {
acc += compress((job ^ seed) & 0xffff);
return (void *)acc;
shuffle.c
/* main() hands jobs to two workers through a small queue. Which worker
* takes a job decides which code path runs, so the profile depends on
* the schedule. */
#include <pthread.h>
#include <stdio.h>
#include <sys/random.h>
#define JOBS 100000
#define SLOTS 8
#define DONE -1
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
static pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
static int queue[SLOTS], head, count;
static unsigned seed;
__attribute__((noinline)) static long checksum(long x)
for (int i = 0; i < 64; i++) {
x = x * 6364136223846793005L + 1442695040888963407L;
return x;
__attribute__((noinline)) static long compress(long x)
while (x > 1) {
x = (x & 1) ? 3 * x + 1 : x / 2;
return x;
static void put(int job)
pthread_mutex_lock(&lock);
while (count == SLOTS) {
pthread_cond_wait(¬_full, &lock);
queue[(head + count++) % SLOTS] = job;
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&lock);
static int take(void)
pthread_mutex_lock(&lock);
while (count == 0) {
pthread_cond_wait(¬_empty, &lock);
int job = queue[head];
head = (head + 1) % SLOTS;
count--;
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&lock);
return job;
static void *worker(void *arg)
long id = (long)arg, acc = 0;
for (int job; (job = take()) != DONE;) {
/* The seed picks which worker checksums and which compresses. */
if ((id ^ seed) & 1) {
acc += checksum(job ^ seed);
} else {
acc += compress((job ^ seed) & 0xffff);
return (void *)acc;
int main(void)
pthread_t t[2];
getrandom(&seed, sizeof seed, 0);
for (long i = 0; i < 2; i++) {
pthread_create(&t[i], NULL, worker, (void *)i);
for (int job = 0; job < JOBS; job++) {
put(job);
for (int i = 0; i < 2; i++) {
put(DONE);
for (int i = 0; i < 2; i++) {
pthread_join(t[i], NULL);
puts("done");
return 0;
一个典型的 Nix derivation 在其 buildPhase 中完成所有三个 PGO 步骤,并将 profile 保存在 $out 中以便我们检查:
pkgs ? import <nixpkgs> { },
pkgs.stdenv.mkDerivation {
name = "shuffle-pgo";
src = ./shuffle.c;
dontUnpack = true;
buildPhase = ''
cp $src shuffle.c
# 1. Build with instrumentation.
cc -O2 -pthread -fprofile-generate -c shuffle.c -o shuffle.o
cc -pthread -fprofile-generate shuffle.o -o shuffle
# 2. Train: the run writes its counters to shuffle.gcda.
./shuffle
# 3. Rebuild using the counters.
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。