当前位置: 首页 > 知识库问答 >
问题:

在基准测试时防止编译器优化

梁丘伟
2023-03-14

我最近遇到了这个精彩的cpp2015演讲cppCon 2015:钱德勒·卡鲁斯“调整C:基准、CPU和编译器!哦,天哪!”

提到的防止编译器优化代码的技术之一是使用以下函数。

static void escape(void *p) {
  asm volatile("" : : "g"(p) : "memory");
}

static void clobber() {
  asm volatile("" : : : "memory");
}

void benchmark()
{
  vector<int> v;
  v.reserve(1);
  escape(v.data());
  v.push_back(10);
  clobber()
}

我在努力理解这一点。问题如下。

1)逃避比重击有什么好处?

2) 从上面的例子来看,clobber()似乎可以防止前面的语句(push_back)以这种方式进行优化。如果是这样,为什么下面的代码片段不正确?

 void benchmark()
 {
     vector<int> v;
     v.reserve(1);
     v.push_back(10);
     clobber()
 }

如果这还不够混乱的话,folly(FB的线程库)有一个更奇怪的实现

相关片段:

template <class T>
void doNotOptimizeAway(T&& datum) {
  asm volatile("" : "+r" (datum));
}

我的理解是,上面的代码片段通知编译器程序集块将写入数据。但是,如果编译器发现这个数据没有使用者,它仍然可以优化生成数据的实体,对吗?

我想这不是常识,任何帮助都是非常感谢的!

共有2个答案

鲁旭
2023-03-14

1)逃避比重击有什么好处?

escape()escape()以以下重要方式补充了clobber()

clobber()的作用仅限于可能通过假想全局根指针访问的内存。换句话说,编译器分配内存的模型是一个通过指针相互引用的块的连接图,所述假想全局根指针充当该图的切入点。(在这个模型中没有考虑内存泄漏,即编译器忽略了曾经可访问的块可能因为指针值丢失而变得不可访问的可能性)。新分配的块不是这样的图的一部分,并且不受clobber()的任何副作用的影响。转义()确保传入的地址属于全局可访问的一组内存块。当应用到新分配的内存块时,转义()具有将其添加到所述图中的效果。

2) 从上面的例子来看,clobber()似乎可以防止前面的语句(push_back)以这种方式进行优化。如果是这样,为什么下面的代码片段不正确?

 void benchmark()
 {
     vector<int> v;
     v.reserve(1);
     v.push_back(10);
     clobber();
 }

隐藏在v.reserve(1)中的分配对Clobber()不可见,直到它通过转义()注册。

公良骁
2023-03-14

tl;drDonoOptimizeAway创建了一个人工的“使用”。

这里有一点术语:“def”(“definition”)是一个语句,它为变量赋值;“use”是一条语句,它使用变量的值来执行某些操作。

如果从def之后的一点开始,程序出口的所有路径都没有遇到使用变量的情况,则该def被称为dead,而dead code Elimination(DCE)pass将删除它。这反过来可能会导致其他def失效(如果该def由于具有可变操作数而被使用),等等。

想象一下在聚合的标量替换(SRA)过程之后的程序,它将本地std::vector转换为两个变量lenptr。在某个时刻,程序会给ptr赋值;那份声明是一份声明。

原来的程序没有对向量做任何处理;换句话说,lenptr都没有任何用途。因此,它们的所有def都已失效,DCE可以删除它们,从而有效地删除所有代码,使基准变得毫无价值。

添加doNotOptimizeAway(ptr)会创建一个人工使用,这会阻止DCE删除def。(顺便说一句,我认为 " ", "g中没有一点应该足够)。

内存加载和存储也可以遵循类似的推理路线:如果没有指向程序末尾的路径,则存储(def)已失效,其中包含来自该存储位置的加载(使用)。由于跟踪任意内存位置比跟踪单个伪寄存器变量要困难得多,编译器会保守地进行推理——如果没有指向程序末尾的路径,那么存储区就死了,这可能会遇到对该存储区的使用。

其中一种情况是存储到内存区域,保证不会出现别名——在释放内存后,不可能使用该存储,因为它不会触发未定义的行为。瞧,没有这样的用途。

因此,编译器可以消除v.push_back(42)。但是有转义-它导致v.data()被认为是任意别名的,如@Leon所述。

本例中的clobber()的目的是人为地使用所有别名内存。我们有一个存储(从push_back(42)),存储到一个全局别名的位置(由于escape(v.data())),因此clobber()可能包含对该存储的使用(low,可以观察到的存储副作用),因此编译器不允许删除该存储。

几个简单的例子:

例一:

void f() {
  int v[1];
  v[0] = 42;
}

这不会生成任何代码。

实例二:

extern void g();

void f() {
  int v[1];
  v[0] = 42;
  g();
}

这只生成对g()的调用,没有内存存储。函数g不可能访问v,因为v没有别名。

例三:

void clobber() {
  __asm__ __volatile__ ("" : : : "memory");
}

void f() {
  int v[1];
  v[0] = 42;
  clobber();
}

与前面的示例一样,没有生成存储,因为v没有别名,并且对clobber的调用内联为零。

例四:

template<typename T>
void use(T &&t) {
  __asm__ __volatile__ ("" :: "g" (t));
}

void f() {
  int v[1];
  use(v);
  v[0] = 42;
}

这一次vescape(即,可以从其他激活帧访问)。然而,存储仍然被删除,因为在它之后,没有对该内存的潜在使用(没有UB)。

例五:

template<typename T>
void use(T &&t) {
  __asm__ __volatile__ ("" :: "g" (t));
}

extern void g();

void f() {
  int v[1];
  use(v);
  v[0] = 42;
  g(); // same with clobber()
}

最后我们得到了存储,因为v转义,编译器必须保守地假设对g的调用可以访问存储的值。

(用于实验)https://godbolt.org/g/rFviMI)

 类似资料:
  • 我最近发现,DRAMs中的比特可以被其中粒子的衰变或宇宙射线随机翻转。我想知道这种错误发生的频率。 不幸的是,我发现的最新统计数据来自1990年(来源),其中指出每128MB内存每月都会发生错误。 由于我找不到任何关于现代ram软错误率的最新统计数据,我尝试用java编写一个程序来测量4GB RAM上的软错误频率。我希望该程序能够检测到分配的4GB内存中的每个软错误,如果它没有以任何方式进行优化的

  • 我得到了一个卡尺基准,如下所示:

  • 11.4. 基准测试 基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数;*testing.B参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。 下面是IsPalindrome函数的基准测试,其中循

  • GoCPPLua (JIT) 策略执行的负载在model_b_test.go中进行基准测试。 测试是: 英特尔 酷睿 i7-6700HQ CPU @ 2.60GHz, 2601 Mhz, 4 核, 8 处理器 go test -bench= -benchmem 的测试结果如下 (op = 一次 Enforce() 调用, ms = 毫秒, KB = 千字节): 测试用例 规则大小 时间开销 (m

  • These benchmarks were designed to measure Javascript running time for Mithril in comparison with other popular Javascript MVC frameworks. Javascript running time is significant because the gzipped siz

  • https://github.com/17koa/koa-benchmark 比较koa和koa2和express性能 Prerequisites for OSX brew install wrk wrk原理 命令行操作 $ wrk -t8 -c1000 -d10 http://127.0.0.1:3000 Test 首次测试,需要安装依赖,会比较慢 make all 不安装依赖,重复测试