在C++中强制执行语句顺序

141

假设我有一些语句需要按照固定顺序执行。我想使用 g++ 的优化级别 2,但某些语句可能会被重新排序。有哪些工具可以强制执行特定的语句顺序?

考虑以下示例。

using Clock = std::chrono::high_resolution_clock;

auto t1 = Clock::now(); // Statement 1
foo();                  // Statement 2
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;
在这个例子中,重要的是按照给定的顺序执行语句1-3。但是,编译器是否可以认为语句2与1和3无关,并按以下方式执行代码?
在这个例子中,执行语句 1-3 的顺序很重要。然而,编译器可能会认为语句2与1和3无关,并按照不同的顺序执行代码。
using Clock=std::chrono::high_resolution_clock;

foo();                  // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3

auto elapsedTime = t2 - t1;

36
如果编译器认为它们是独立的,但事实并非如此,那么这个编译器就存在问题,你应该使用更好的编译器。 - David Schwartz
19
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0342r0.html - Howard Hinnant
3
如果定义这样的指令,并且调整别名规则以豁免在数据栏杆之后读取已写入的数据,则标准C语言的语义能力将大大提高。 - supercat
5
在这种情况下,它是关于测量运行foo所需的时间,编译器可以在重新排序时忽略它,就像它可以忽略来自不同线程的观察一样。 - CodesInChaos
2
易变的函数指针是你的好朋友。此外,如果你在进行分析,使用像Valgrind这样的工具。 - lorro
显示剩余9条评论
7个回答

137
这次我想尝试提供一个更加全面的答案,这是在与C++标准委员会讨论后得出的。除了作为C++委员会成员外,我还是LLVM和Clang编译器的开发者。
从根本上说,没有办法使用障碍或一些操作来实现这些转换。根本问题在于像整数加法这样的操作语义对实现方面是完全已知的:它可以模拟它们,知道它们不能被正确的程序观察到,并且总是可以自由地移动它们。
我们可以尝试防止这种情况发生,但这将产生极其负面的结果,并最终失败。
首先,唯一防止编译器这样做的方法是告诉它所有这些基本操作都是可观察的。问题在于,这将排除绝大多数编译器优化。在编译器内部,我们基本上没有好的机制来模拟观察到"时间" 但不观察其他东西。我们甚至没有一个良好的模型来描述"哪些操作需要时间"。比如,将32位无符号整数转换为64位无符号整数需要时间吗?在x86-64上不需要时间,但是在其他架构上需要非零时间。这里没有普遍正确的答案。
但是即使我们通过某些英勇的行为成功地防止了编译器重新排序这些操作,也不能保证这就足够了。考虑在x86机器上执行C++程序的有效和符合规范的方式:DynamoRIO。这是一个动态评估程序的系统。它可以在线优化,甚至能够在时间之外推测执行整个基本算术指令范围内的操作。而且这种行为不仅限于动态评估器,实际的x86 CPU也会推测(数量更少的)指令并动态重排序它们。
本质上的想法是算术不可观察(即使在时间层面),在计算机的各个层面都是普遍存在的。它对编译器、运行时甚至硬件都是如此。强制其可观察将会极大地限制编译器,同时也会极大地限制硬件。
但是这一切都不应该让你失去希望。当你想计时基本数学操作时,我们有经过深入研究的可靠技术。通常这些技术用于"微基准测试"。我在CppCon2015上做了一个关于这方面的演讲:https://youtu.be/nXaxk27zwlk 所示的技术也由各种微基准库提供,如Google的:https://github.com/google/benchmark#preventing-optimization
这些技术的关键在于专注于数据。您使计算机输入对优化器不透明,使计算结果对优化器也不透明。一旦做到了这一点,您就可以可靠地计时。让我们看一个现实版本的原始问题中的示例,但是将foo的定义完全显示给实现。我还从Google基准库中提取了DoNotOptimize的(非便携式)版本,您可以在此处找到:https://github.com/google/benchmark/blob/v1.0.0/include/benchmark/benchmark_api.h#L208
#include <chrono>

template <class T>
__attribute__((always_inline)) inline void DoNotOptimize(const T &value) {
  asm volatile("" : "+m"(const_cast<T &>(value)));
}

// The compiler has full knowledge of the implementation.
static int foo(int x) { return x * 2; }

auto time_foo() {
  using Clock = std::chrono::high_resolution_clock;

  auto input = 42;

  auto t1 = Clock::now();         // Statement 1
  DoNotOptimize(input);
  auto output = foo(input);       // Statement 2
  DoNotOptimize(output);
  auto t2 = Clock::now();         // Statement 3

  return t2 - t1;
}

在这里,我们确保输入数据和输出数据在计算foo周围被标记为不可优化,并且只有在这些标记周围计算时间才会被计算。由于您使用数据夹紧计算,因此保证它保持在两个时间之间,但计算本身可以被优化。最近的Clang/LLVM版本生成的x86-64汇编代码如下:

% ./bin/clang++ -std=c++14 -c -S -o - so.cpp -O3
        .text
        .file   "so.cpp"
        .globl  _Z8time_foov
        .p2align        4, 0x90
        .type   _Z8time_foov,@function
_Z8time_foov:                           # @_Z8time_foov
        .cfi_startproc
# BB#0:                                 # %entry
        pushq   %rbx
.Ltmp0:
        .cfi_def_cfa_offset 16
        subq    $16, %rsp
.Ltmp1:
        .cfi_def_cfa_offset 32
.Ltmp2:
        .cfi_offset %rbx, -16
        movl    $42, 8(%rsp)
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, %rbx
        #APP
        #NO_APP
        movl    8(%rsp), %eax
        addl    %eax, %eax              # This is "foo"!
        movl    %eax, 12(%rsp)
        #APP
        #NO_APP
        callq   _ZNSt6chrono3_V212system_clock3nowEv
        subq    %rbx, %rax
        addq    $16, %rsp
        popq    %rbx
        retq
.Lfunc_end0:
        .size   _Z8time_foov, .Lfunc_end0-_Z8time_foov
        .cfi_endproc


        .ident  "clang version 3.9.0 (trunk 273389) (llvm/trunk 273380)"
        .section        ".note.GNU-stack","",@progbits

在这里,您可以看到编译器将对 foo(input)的调用优化为单个指令 addl%eax,%eax ,但没有将其移到时间外或完全消除它,尽管输入是常数。
希望这有所帮助,C ++标准委员会正在考虑标准化与 DoNotOptimize 类似的API的可能性。

2
谢谢你的回答。我已将其标记为新的最佳答案。我本可以早些这样做,但我已经好几个月没有阅读过这个stackoverflow页面了。我非常有兴趣使用Clang编译器制作C++程序。除其他外,我喜欢在Clang中可以在变量名中使用Unicode字符。我想我会在Stackoverflow上问更多关于Clang的问题。 - S2108887
5
我了解这样的操作可以防止完全优化掉foo,但能否详细说明一下为什么这可以防止调用Clock::now()被重新排序以相对于foo()?是否是因为优化器必须假定DoNotOptimizeClock::now()可以访问并可能修改某些公共全局状态,从而将它们与输入和输出联系起来?或者你是依赖于优化器实现目前的某些限制呢? - MikeMB
3
在这个例子中,DoNotOptimize 是一个人工合成的“可观察”事件。这就好像它在某个终端上打印了输入的表示形式的可见输出一样。由于读取时钟也是可观察的(您正在观察时间流逝),因此不能重新排序它们,否则将改变程序的可观察行为。 - Chandler Carruth
1
我仍然不太清楚“observable”这个概念,如果 foo 函数正在执行一些操作(比如从套接字读取数据),这可能会被阻塞一段时间,这算作可观察操作吗?由于 read 操作不是“完全已知”的操作(对吗?),所以代码是否会按顺序执行? - user2269707
“根本问题在于像整数加法这样的操作语义完全由实现方知晓。”但我认为问题不在于整数加法的语义,而在于调用函数foo()的语义。除非foo()在同一编译单元中,否则它如何知道foo()和clock()之间没有交互? - Dave
显示剩余16条评论

61

概述:

似乎没有一种可靠的方法可以防止重新排序,但只要未启用链接时/完整程序优化,将调用的函数定位在单独的编译单元中似乎是一个相当好的选择。(至少对于GCC来说,尽管逻辑会表明其他编译器也很可能如此。)这是以函数调用的代价为代价的——内联代码根据定义在同一编译单元中并且容易被重新排序。

原始答案:

GCC在-O2优化下重新排序调用:

#include <chrono>
static int foo(int x)    // 'static' or not here doesn't affect ordering.
{
    return x*2;
}
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

GCC 5.3.0:

g++ -S --std=c++11 -O0 fred.cpp :

_ZL3fooi:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %ecx, 16(%rbp)
        movl    16(%rbp), %eax
        addl    %eax, %eax
        popq    %rbp
        ret
_Z4fredi:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $64, %rsp
        movl    %ecx, 16(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -16(%rbp)
        movl    16(%rbp), %ecx
        call    _ZL3fooi
        movl    %eax, -4(%rbp)
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movq    %rax, -32(%rbp)
        movl    -4(%rbp), %eax
        addq    $64, %rsp
        popq    %rbp
        ret

但是:

g++ -S --std=c++11 -O2 fred.cpp

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        call    _ZNSt6chrono3_V212system_clock3nowEv
        leal    (%rbx,%rbx), %eax
        addq    $32, %rsp
        popq    %rbx
        ret

现在,将foo()作为外部函数:
#include <chrono>
int foo(int x);
int fred(int x)
{
    auto t1 = std::chrono::high_resolution_clock::now();
    int y = foo(x);
    auto t2 = std::chrono::high_resolution_clock::now();
    return y;
}

g++ -S --std=c++11 -O2 fred.cpp :

_Z4fredi:
        pushq   %rbx
        subq    $32, %rsp
        movl    %ecx, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %ecx
        call    _Z3fooi
        movl    %eax, %ebx
        call    _ZNSt6chrono3_V212system_clock3nowEv
        movl    %ebx, %eax
        addq    $32, %rsp
        popq    %rbx
        ret

但是,如果这与-flto(链接时优化)相关联:

0000000100401710 <main>:
   100401710:   53                      push   %rbx
   100401711:   48 83 ec 20             sub    $0x20,%rsp
   100401715:   89 cb                   mov    %ecx,%ebx
   100401717:   e8 e4 ff ff ff          callq  100401700 <__main>
   10040171c:   e8 bf f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401721:   e8 ba f9 ff ff          callq  1004010e0 <_ZNSt6chrono3_V212system_clock3nowEv>
   100401726:   8d 04 1b                lea    (%rbx,%rbx,1),%eax
   100401729:   48 83 c4 20             add    $0x20,%rsp
   10040172d:   5b                      pop    %rbx
   10040172e:   c3                      retq

3
MSVC和ICC也会这样做。只有Clang似乎能保留原始的顺序。 - Cody Gray
4
你没有在任何地方使用t1和t2,因此编译器可能认为结果可以被丢弃并重新排列代码。 - phuclv
3
@Niall - 我无法提供更具体的信息,但我认为我的评论暗示了其根本原因:编译器知道foo()不会影响now(),反之亦然,因此进行了重新排序。各种涉及extern作用域函数和数据的实验似乎证实了这一点。这包括使静态foo()依赖于文件作用域变量N-如果N被声明为静态,则会发生重新排序,而如果它被声明为非静态(即可见于其他编译单元,并因此可能受到extern函数如now()的副作用的影响),则不会发生重新排序。 - Jeremy
3
除了调用本身没有被省略外,我怀疑这是因为编译器不知道它们的副作用可能是什么,但它确实知道这些副作用不能影响foo()的行为。 - Jeremy
3
最后说明一点:指定“-flto”(链接时优化)即使在其他情况下不进行重新排序,也会导致重新排序。 - Jeremy
显示剩余14条评论

20

重新排序可能由编译器或处理器完成。

大多数编译器提供了一种平台特定的方法来防止读写指令的重新排序。在gcc上,这是

asm volatile("" ::: "memory");

(此处获取更多信息)

请注意,这只间接地防止重排序操作,只要它们依赖于读/写操作。

实际上,我还没有看到过系统调用Clock::now()具有与这种屏障相同的效果的系统。您可以检查生成的汇编代码来确保。

然而,很常见的是,在编译时评估测试函数。为了强制执行“真实”的执行,您可能需要从I/O或volatile读取中派生foo()的输入。


另一种选择是禁用foo()的内联 - 再次说,这是特定于编译器的,并且通常不可移植,但会产生相同的效果。

在gcc上,这将是__attribute__ ((noinline))


@Ruslan提出了一个基本问题:这种测量有多现实?

执行时间受许多因素影响:其中之一是我们正在运行的实际硬件,另一个是对共享资源(如缓存、内存、磁盘和CPU核心)的并发访问。

因此,我们通常会采取以下措施来获得可比较的时间:确保它们具有低误差边界,使它们有些人工的。

“热缓存”与“冷缓存”的执行性能可以轻松相差一个数量级 - 但在现实中,它将是介于它们之间的某种状态(“微温”?)


2
你使用 asm 的黑客技巧会影响计时器调用之间语句的执行时间:在内存破坏后的代码必须重新从内存加载所有变量。 - Ruslan
@Ruslan:这是他们的黑客行为,不是我的。有不同程度的清除操作,而像这样做是为了获得可重复的结果而不可避免的。 - peterchen
2
请注意,使用'asm'的黑客只有在涉及内存操作时才有效,并且该OP对此更感兴趣。请查看我的答案以获取更多详细信息。 - Chandler Carruth

11

C++语言以多种方式定义了什么是可观察的。

如果foo()没有任何可观察的行为,那么它可以完全被消除。如果foo()只进行计算并将值存储在“本地”状态中(不管是在堆栈上还是在对象中),并且编译器能够证明没有安全派生指针可以进入Clock::now()代码,则移动Clock::now()调用没有可观察的后果。

如果foo()与文件或显示器交互,并且编译器无法证明Clock::now()是否与文件或显示器进行交互,则无法重排序,因为与文件或显示器的交互是可观察的行为。

虽然您可以使用特定于编译器的技巧来强制代码不移动(如内联汇编),但另一种方法是尝试智胜您的编译器。

创建一个动态加载库。在涉及的代码之前加载它。

该库仅公开一件事:

namespace details {
  void execute( void(*)(void*), void *);
}

并像这样包装它:

template<class F>
void execute( F f ) {
  struct bundle_t {
    F f;
  } bundle = {std::forward<F>(f)};

  auto tmp_f = [](void* ptr)->void {
    auto* pb = static_cast<bundle_t*>(ptr);
    (pb->f)();
  };
  details::execute( tmp_f, &bundle );
}

这个函数打包了一个零元 lambda,并使用动态库在编译器无法理解的上下文中运行它。

在动态库中,我们执行以下操作:

void details::execute( void(*f)(void*), void *p) {
  f(p);
}

这相当简单。

现在为了重新排序对 execute 的调用,它必须理解动态库,而在编译您的测试代码时无法实现。

它仍然可以消除没有副作用的 foo(),但是有得必有失。


20
如果这句话不是已经进入了“兔子洞”的标志,那我也不知道还有什么会是了。另一种方法是尝试让你的编译器被愚弄。 - Cody Gray
1
我认为值得注意的是,代码块执行所需的时间不被视为编译器必须维护的“可观察”行为。如果代码块的执行时间是“可观察”的,那么就不允许任何形式的性能优化。虽然定义一个“因果关系屏障”对于C和C++来说会很有帮助,这将要求编译器在屏障之后不执行任何代码,直到生成的代码处理了屏障之前的所有副作用(想要确保数据已完全... - supercat
1
通过硬件缓存传播的写入需要使用硬件特定的手段来实现,但是如果没有屏障指令来确保编译器跟踪的所有挂起写入都必须在硬件被要求确保所有已发布的写入完成之前发布到硬件上,那么等待所有已发布写入完成的硬件特定手段将是无用的。我不知道在任何一种语言中如何做到这一点,除非使用虚拟的volatile访问或调用外部代码。 - supercat

3
不能。根据C++标准[intro.execution]:
每个完整表达式中相关的值计算和副作用在执行下一个完整表达式之前被排序。完整表达式基本上是以分号结束的语句。如上述规则所述,语句必须按顺序执行。编译器可以在语句内部具有更自由的控制权(即在某些情况下允许以非从左向右或其他特定方式评估构成语句的表达式)。
请注意,在这里不满足as-if规则适用的条件。认为任何编译器都能够证明重新排序获取系统时间的调用不会影响可观察程序行为是不合理的。如果存在两个获取时间的调用可以重新排序而不改变观察到的行为的情况,那么实际上产生一个具有足够理解程序并能够确定地推断此问题的编译器将会非常低效。

12
仍然存在“似乎”规则。 - M.M
18
根据“好像规则”,编译器可以对代码进行任何操作,只要不改变可观察行为。执行时间不可观察。因此,只要结果相同,它可以重新排列任意行代码(大多数编译器会明智地避免重新排序时间调用,但这并非必需)。 - Revolver_Ocelot
6
执行时间不可观察,这很奇怪。从实际的非技术角度来看,执行时间(又称“性能”)是非常可观察的。 - Frédéric Hamidi
3
取决于你如何测量时间。在标准的C++中,无法测量执行某些代码所需的时钟周期数。 - Peter
3
@dba,你混淆了几个概念。连接器不再能够生成Win16应用程序,这是确实的,但那是因为它们已经移除了生成此类二进制文件的支持。Win16应用程序不使用PE格式。这并不意味着编译器或连接器具有有关API函数的特殊知识。另一个问题与运行时库有关。完全没有问题可以让最新版本的MSVC生成在NT 4上运行的二进制文件。我已经做过了。问题出现在你尝试链接CRT时,因为它调用了一些不可用的函数。 - Cody Gray
显示剩余22条评论

1

编号。

有时,根据“好像”规则,语句可以重新排列。这不是因为它们在逻辑上彼此独立,而是因为这种独立性允许进行这样的重新排序而不改变程序的语义。

显然移动获取当前时间的系统调用并不满足该条件。一个有意或无意地这样做的编译器是不合规的,而且非常愚蠢。

一般来说,我不希望任何导致系统调用的表达式被过度优化的编译器“反复思考”。它并不了解那个系统调用的具体情况。


5
我同意这是个荒谬的想法,但我不会称之为“不符合规范”。编译器可以了解特定系统上的系统调用以及其是否具有副作用。我希望编译器不要为了覆盖常见用例而重新排序此类调用,允许更好的用户体验,而不是因为标准禁止这样做。 - Revolver_Ocelot
4
除了复制省略之外,改变程序语义的优化行为是不符合标准的,无论你是否同意。 - Lightness Races in Orbit
6
int x = 0; clock(); x = y*2; clock();这个简单的情况下,clock()代码和变量x之间没有定义的交互方式。根据C++标准,clock()不需要知道它所执行的操作,它可以检查堆栈(并注意到计算发生的时候),但是这不是C++的问题。 - Yakk - Adam Nevraumont
5
进一步说,采用Yakk的观点:确实,重新排列系统调用的顺序(使第一个的结果分配给t2,第二个分配给t1),如果使用这些值,则是不符合规范和愚蠢的。然而,这个答案忽略了一个事实,符合规范的编译器有时可以重新排列系统调用之外的代码。在这种情况下,只要编译器知道foo()的作用(例如因为它已经被内联),因此知道(粗略地说),它是一个纯函数,那么它就可以移动它。 - Steve Jessop
1
大致上来说,这是因为没有保证实际实现(虽然不是抽象机器)在系统调用之前不会进行“y*y”的推测计算,仅仅是为了好玩。同样也不能保证实际实现不会在稍后使用这个推测计算的结果,在任何使用“x”的地方之间什么都不做,即在对“clock()”的调用之间。对于内联函数“foo”所做的任何事情也是如此,只要它没有副作用并且不能依赖可能被“clock()”改变的状态。 - Steve Jessop
显示剩余3条评论

0

noinline函数 + 内联汇编黑盒 + 完整数据依赖关系

这是基于https://dev59.com/QloU5IYBdhLWcg3wIkeI#38025837的,但因为我没有看到任何明确的理由说明为什么::now()不能在那里重新排序,所以我宁愿有点偏执地将其放在一个noinline函数中与asm一起。

这样我很确定重排序不会发生,因为noinline“绑定”了::now和数据依赖关系。

main.cpp

#include <chrono>
#include <iostream>
#include <string>

// noinline ensures that the ::now() cannot be split from the __asm__
template <class T>
__attribute__((noinline)) auto get_clock(T& value) {
    // Make the compiler think we actually use / modify the value.
    // It can't "see" what is going on inside the assembly string.
    __asm__ __volatile__ ("" : "+g" (value));
    return std::chrono::high_resolution_clock::now();
}

template <class T>
static T foo(T niters) {
    T result = 42;
    for (T i = 0; i < niters; ++i) {
        result = (result * result) - (3 * result) + 1;
    }
    return result;
}

int main(int argc, char **argv) {
    unsigned long long input;
    if (argc > 1) {
        input = std::stoull(argv[1], NULL, 0);
    } else {
        input = 1;
    }

    // Must come before because it could modify input
    // which is passed as a reference.
    auto t1 = get_clock(input);
    auto output = foo(input);
    // Must come after as it could use the output.
    auto t2 = get_clock(output);
    std::cout << "output " << output << std::endl;
    std::cout << "time (ns) "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count()
              << std::endl;
}

GitHub 上游

编译并运行:

g++ -ggdb3 -O3 -std=c++14 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out 1000
./main.out 10000
./main.out 100000

这种方法唯一的小缺点是我们在一个内联方法上添加了一个额外的callq指令。 objdump -CD显示main包含:
    11b5:       e8 26 03 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>
    11ba:       48 8b 34 24             mov    (%rsp),%rsi
    11be:       48 89 c5                mov    %rax,%rbp
    11c1:       b8 2a 00 00 00          mov    $0x2a,%eax
    11c6:       48 85 f6                test   %rsi,%rsi
    11c9:       74 1a                   je     11e5 <main+0x65>
    11cb:       31 d2                   xor    %edx,%edx
    11cd:       0f 1f 00                nopl   (%rax)
    11d0:       48 8d 48 fd             lea    -0x3(%rax),%rcx
    11d4:       48 83 c2 01             add    $0x1,%rdx
    11d8:       48 0f af c1             imul   %rcx,%rax
    11dc:       48 83 c0 01             add    $0x1,%rax
    11e0:       48 39 d6                cmp    %rdx,%rsi
    11e3:       75 eb                   jne    11d0 <main+0x50>
    11e5:       48 89 df                mov    %rbx,%rdi
    11e8:       48 89 44 24 08          mov    %rax,0x8(%rsp)
    11ed:       e8 ee 02 00 00          callq  14e0 <auto get_clock<unsigned long long>(unsigned long long&)>

所以我们可以看到,foo 已经被内联了,但 get_clock 没有并且围绕它。
然而,get_clock 本身非常高效,由一个单独的叶子调用优化指令组成,甚至不会触及栈:
00000000000014e0 <auto get_clock<unsigned long long>(unsigned long long&)>:
    14e0:       e9 5b fb ff ff          jmpq   1040 <std::chrono::_V2::system_clock::now()@plt>

由于时钟精度本身是有限的,我认为您不太可能注意到一个额外的jmpq所带来的时间影响。请注意,由于::now()位于共享库中,因此仍需要一个call

使用具有数据依赖性的内联汇编调用::now()

这将是最有效的解决方案,甚至可以克服上述额外的jmpq

不幸的是,如在扩展内联ASM中调用printf所示,正确实现这一点非常困难。

如果您的时间测量可以直接在内联汇编中完成而不需要调用,则可以使用此技术。例如,gem5魔法插装指令、x86 RDTSC(不确定现在是否还具有代表性)以及可能其他性能计数器都是这种情况。
相关主题: 在GCC 8.3.0、Ubuntu 19.04上进行了测试。

1
通常情况下,您不需要使用"+m"来强制触发溢出/重新加载操作。使用"+r"是一种更高效的方法,它可以使编译器将值实例化,然后假定变量已更改。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接