返回资讯中心
外部精选
软件工程
#开发者实时流

文章探讨如何让 PGO 优化实现可复现构建

来源标题:Making profile-guided optimization reproducible

Profile-guided optimization(PGO)能为程序带来明显加速,作者构建的 PGO 版 sqlite3 在特定负载下提速 1.32 倍。但 PGO 构建不可复现,导致 Python、GCC 等在 Nixpkgs 和 Guix 中默认关闭该优化。文章讨论了在包管理器中让 PGO 可复现的问题。

Hacker News 最新ingve2026/11/8 · 今天30 分钟阅读

以下正文由 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(&not_full, &lock);
 queue[(head + count++) % SLOTS] = job;
 pthread_cond_signal(&not_empty);
 pthread_mutex_unlock(&lock);
static int take(void)
 pthread_mutex_lock(&lock);
 while (count == 0) {
 pthread_cond_wait(&not_empty, &lock);
 int job = queue[head];
 head = (head + 1) % SLOTS;
 count--;
 pthread_cond_signal(&not_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 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →