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

C中的无序执行#

米飞龙
2023-03-14

我有以下片段:

static long F(long a, long b, long c, long d) 
{
    return a + b + c + d;
}

其中生成:

<Program>$.<<Main>$>g__F|0_0(Int64, Int64, Int64, Int64)
    L0000: add rdx, rcx
    L0003: lea rax, [rdx+r8]
    L0007: add rax, r9
    L000a: ret

如果我从本(§无序执行)手册中正确理解:以上代码转换为<代码>((a b)c)d 。为了计算这个,CPU必须等待第一个括号和第二个括号,以此类推。在这里,我们看到LEA位于中间,这意味着它们不能并行执行(如果我理解正确的话)。因此,作者建议:

在“独立”对上写括号:

static long G(long a, long b, long c, long d) 
{
    return (a + b) + (c + d);
}

但这会生成相同的程序集:

<Program>$.<<Main>$>g__G|0_1(Int64, Int64, Int64, Int64)
    L0000: add rdx, rcx
    L0003: lea rax, [rdx+r8]
    L0007: add rax, r9
    L000a: ret

相反,这是GCC(O2)为C生成的代码:

int64_t
f(int64_t a, int64_t b, int64_t c, int64_t d) {
        return a + b + c + d;
}

int64_t
g(int64_t a, int64_t b, int64_t c, int64_t d) {
        return (a + b) + (c + d);
}

以下是输出:

f: 
        add     rcx, rdx        ; I guess -O2 did the job for me.
        add     rcx, r8         ; I guess -O2 did the job for me.
        lea     rax, [rcx+r9]
        ret
g:
        add     rcx, rdx
        add     r8, r9
        lea     rax, [rcx+r8]
        ret
  • 我是否正确理解了手册?2个ADD是否应该相互关联(中间没有LEA)?如果是,我如何提示C#编译器不要忽略我的括号?
  • 这是SharpLab链接
  • 这是Gotbolt链接

共有1个答案

朱啸
2023-03-14

整数加法是关联的。编译器可以利用这一点(“好像规则”),而不管源代码级操作的顺序如何。

(不幸的是,似乎大多数编译器在这方面做得很糟糕,即使您巧妙地编写源代码,情况也会变得更糟。)

在ami中整数溢出没有副作用;即使在MIPS等目标上,在符号溢出上添加陷阱,编译器也使用addu,但没有,因此他们可以进行优化。(在C中,编译器可以假设源代码级操作顺序永远不会溢出,因为那将是未定义的行为。因此,他们可以在拥有它的ISA上使用陷阱add,用于C抽象机器中相同输入发生的计算。但是,即使gcc-fwrapv给符号整数溢出定义明确的2的补码环绕行为不是默认的,编译器确实使用可能允许静默环绕而不是陷阱的指令。主要是这样,他们不必关心任何给定的操作是否对出现在C抽象机器中的值进行。UB并不意味着需要故障;-fsanitize=未定义需要额外的代码才能实现。)

e、 g.INT\u MAX INT\u MIN 1可计算为INT\u MAX 1(溢出到INT\u MIN),然后。INT\u MIN在2的补码机上溢出回0,或按源代码顺序无溢出。相同的最终结果,这就是从操作中逻辑上可见的全部内容。

带有无序exec的CPU不会尝试重新关联指令,但它们遵循asm/机器代码中的依赖关系图。

(一方面,这对于硬件来说太多了,无法动态考虑,另一方面,每个操作的标志输出确实取决于您创建的临时性,中断可能到达任何一点。因此,当所有旧指令完成时,需要在指令边界处恢复适当的体系结构状态。这意味着编译器的工作是公开指令级asm中的并行性,而不是硬件使用数学来创建它。另请参阅现代微处理器90分钟指南!还有这个答案)

最糟糕的是,自食其果/对您进行源代码级优化的尝试感到悲观,至少在这种情况下是这样。

>

  • C#:删除ILP,即使它存在于源中;将(a b)(c d)序列化为一个线性操作链;3周期延迟。

    clang12.0:相同,序列化两个版本。

    MSVC:相同,序列化两个版本。

    GCC11。1表示签名的int64\u t:保留源操作顺序。这是一个长期存在的GCC遗漏优化缺陷,其优化器由于某种原因避免引入签名溢出,即使是临时性的,就像在抽象机器中的UB在创建一个在抽象机器上运行的具体实现时所创造的promise/保证/优化机会一样。尽管GCC知道它可以自动矢量化int和;这只是在标量表达式中重新排序,其中一些过于保守的检查将带符号整数与浮点合并为非关联。

    GCC11。1对于uint64\u t或带有fwrapv:视为关联,并以相同方式编译f和g。使用大多数优化选项进行序列化(包括MIPS或PowerPC等其他ISA),但碰巧会创建ILP。(这并不意味着只有AMD Zen是超标量的,这意味着GCC错过了优化漏洞!)

    ICC 2021.1.2:即使在线性源代码版本(f)中也创建ILP,但最后一步使用add/mov而不是LEA:/

    clang/MSVC/ICC的锁销。

    GCC用有/无符号或带fwrapv的螺栓。

    理想的做法是从两个独立的加法开始,然后组合成对的加法。这三个加法中的一个应该使用lea来得到RAX的结果,但它可以是这三个加法中的任何一个。在独立函数中,您可以销毁任何传入的arg传递寄存器,没有真正的理由避免覆盖其中的两个而不是一个。

    您只需要一个LEA,因为2寄存器寻址模式使其比ADD指令更长。

  •  类似资料:
    • 问题内容: 我正在阅读有关章节的内容。似乎您可以按以下方式列出节和段之间的映射。 我的问题, 我不明白程序头是什么意思?它们与细分有何关系? 段到段的映射很清楚。但是有人可以命名吗?我只看到数字。我确定了代码段(03),数据段(02)和堆栈(07)。 问题答案: 了解它的输出将帮助您了解文件的格式。请参考本文件。 就了解如何解释此链接的输出而言可能会有帮助。 关于您的问题2,此链接描述了细分。在该

    • 问题内容: 我应该如何从我的程序中运行另一个程序?我需要能够将数据写入启动的程序中(并可能从中读取) 我不确定这是否是标准的C函数。我需要应该在Linux下工作的解决方案。 问题答案: 您要使用。它为您提供了一个单向管道,您可以使用该管道访问程序的stdin和stdout。 popen是现代unix和类似unix的操作系统的标准配置,其中Linux是其中之一:-) 类型 在终端上阅读有关它的更多信

    • 该程序的输出: 是: 为什么当 开始时 不是 1?

    • 本文向大家介绍C ++程序中的迭代器无效,包括了C ++程序中的迭代器无效的使用技巧和注意事项,需要的朋友参考一下 在本教程中,我们将讨论一个程序,以了解C ++中的迭代器失效。 在容器对象的元素上进行迭代时,有时如果我们不应用绑定检查,它可能会失效。这主要是由于容器对象的形状和大小的变化而发生的。 示例 输出结果 (可能还会发生,添加新元素时,矢量将被复制到新位置,而我们的迭代器仍指向旧位置,这

    • 关于system("pause"),我知道它可以暂停命令行界面,等待用户的输入。但是对于以下代码,我遇到一些问题: 我的运行结果如下: 其中我按了三次Control-C才结束掉程序。难道pause命令不是把进程阻塞了么?为什么还能继续执行循环呢?这三次Control-C时各自发生了什么事情? 此外,我尝试对程序进行调试,发现在第一个和最后一个Control-C时,VS会报异常: 但中间一个并不会。

    • 问题内容: 想知道是否有人知道一种在运行时从C#代码执行Java命令行程序的好方法吗? 它与执行本机.EXE文件相同吗? 它会同步还是异步运行(这意味着我可能必须等待线程完成才能找到结果) 具体来说,我想从服务器端的Web应用程序中调用一个小工具(恰好用Java编写),以对文本文件进行一些处理。我想等待它完成,因为在Java程序完成对文本文件的处理之后,我想获取处理后的文本,并在C#应用程序中使用