如何编写编译器“可理解的” C代码?

问题描述 投票:1回答:2

最近,我不得不为关键的实时功能编写代码,而我只使用了几个__ builtin _...函数。我知道这类代码不可移植,因为并非所有编译器都支持“ __ builtin _...”函数或语法。我想知道是否有办法在纯C语言中编写代码,以便编译器能够识别它并使用一些内部“ __ builtin _...”-like函数?

下面是我做了一次小实验的描述,但我的问题是:

  • [是否有任何技巧,最知名的方法,指南来编写可移植的C代码,以便编译器能够检测(让编译器的bug放在一旁,而无需考虑编译器的错误)并使用目标CPU架构的最大能力。

例如Dword中的反向字节(以使第一个字节成为最后一个字节,最后一个字节成为第一个字节,依此类推),x86_64体系结构具有专用的汇编指令-bswap 。我尝试了4种不同的选择:

bswap

所有选项均提供相同的功能。我用不同的编译器和不同的优化级别对其进行了编译,结果不是很好:

  1. GCC-很棒!对于#include <stdint.h> #include <stdlib.h> typedef union _helper_s { uint32_t val; uint8_t bytes[4]; } helper_u; uint32_t reverse(uint32_t d) { helper_u b; uint8_t temp; b.val = d; temp = b.bytes[0]; b.bytes[0] = b.bytes[3]; b.bytes[3] = temp; temp = b.bytes[1]; b.bytes[1] = b.bytes[2]; b.bytes[2] = temp; return b.val; } uint32_t reverse1(uint32_t d) { helper_u b; uint8_t temp; b.val = d; for (size_t i = 0; i < sizeof(uint32_t) / 2; i++) { temp = b.bytes[i]; b.bytes[i] = b.bytes[sizeof(uint32_t) - i - 1]; b.bytes[sizeof(uint32_t) - i - 1] = temp; } return b.val; } uint32_t reverse2(uint32_t d) { return (d << 24) | (d >> 24 ) | ((d & 0xFF00) << 8) | ((d & 0xFF0000) >> 8); } uint32_t reverse3(uint32_t d) { return __builtin_bswap32(d); } -O3优化级别,它对所有功能都给出相同的结果:

    -O3
  2. Clang令我有些失望。使用-Os时,其结果与GCC相同,但是使用-Os时,它完全失去了reverse: mov eax, edi bswap eax ret reverse1: mov eax, edi bswap eax ret reverse2: mov eax, edi bswap eax ret reverse3: mov eax, edi bswap eax ret 中的路径。它无法识别模式,因此产生的最佳二进制数较少:

    -O3

    实际上-O3-Os之间的区别在于-Osreverse1的“循环展开”版本,因此我假设使用reverse1: # @reverse1 lea rax, [rsp - 8] mov dword ptr [rax], edi mov ecx, 3 .LBB1_1: # =>This Inner Loop Header: Depth=1 mov sil, byte ptr [rax] mov dl, byte ptr [rsp + rcx - 8] mov byte ptr [rax], dl mov byte ptr [rsp + rcx - 8], sil dec rcx inc rax cmp rcx, 1 jne .LBB1_1 mov eax, dword ptr [rsp - 8] ret 编译器甚至没有尝试展开或尝试预期reverse循环的目的。

  3. 使用ICC,情况变得更糟,因为在reverse1reverse优化级别下,它都无法识别reverse1-Os函数中的模式。

P.S。

[我经常听到人们说必须编写代码,这样即使是初级程序员也可以轻松理解它,并且modern编译器足够“聪明”,可以进行优化。现在,我有一个证据表明这是不正确的(或者至少并非总是如此)。

c compiler-optimization built-in
2个回答
1
投票

据我所知,执行此操作的正确方法是使用条件编译。

我的建议是使用标准C语言编写普通的普通代码作为默认代码,以确保可维护性和所有编译器都可以处理的后备路径。仅在有必要时才使用条件编译,以针对特定编译器进行优化,并在注释中说明出现异常的原因。


1
投票

用于for的技术是相当惯用的(例如reverse),并且您自己的测试表明,在所测试的所有系统上均已对其进行了适当的优化。为了使实现更易于理解,可以引入更多的空格,并遵循更常规的模式。

reverse1

-O3

-O3

至您的特定要点:

是否有任何技巧,最知名的方法,准则来编写可移植的C代码,以便编译器能够检测(让编译器的bug放在一旁,而不管)模式并使用目标CPU架构的最大能力。

最重要的是尝试编写惯用代码。将代码判断为understandable有点主观。在我看来似乎很清楚的事情可能会让其他人难以理解(反之亦然)。但是,在适当的时候,应该遵循C编程中的一些常见习惯用法。

[不幸的是,我的脑海中没有方便的成语清单。但是,我可以说,我通过阅读The C Programming Language(当然是K&R)学习了C语言。我是C编程常见问题解答(由Steve Summit撰写的)的狂热读者。

但是,通过阅读和理解开源C项目以及您所在公司的源代码库,可以找到关于C习语的很好的资源。遵循后者有一个额外的好处,就是您添加的任何遵循现有约定的代码自然会增加公司中其他人理解它的机会。

我经常听到人们说必须编写代码,这样即使是初级程序员也可以轻松理解它,而现代编译器“足够聪明”,可以进行优化。现在,我有一个证据表明这是不正确的(或者至少并非总是如此)。

编译器只是程序,因此它们无法理解您的想法。将对编译器进行编程,以查找AST中的特定模式,并应用优化以将树转换为它认为更优化的树。同样,窥孔优化器将在生成的机器指令中查找模式,然后将其转换为更少的等效指令。

但是只有当生成的树或生成的指令遵循可识别的模式时,这些转换才可能进行。这些模式通常是通过分析现实世界的软件来确定某些操作生成的代码来确定的。如果您的代码未产生可被编译器识别的代码,则可能会部分损失掉编译器的帮助以进行优化。

因此,是另一个尝试编写惯用的C代码的原因。

现在,可以争辩说,强迫自己写惯用的C是微优化的一种形式。您应该尝试教编译器如何优化编写代码的方式,还是让编译器教您如何编写知道如何优化的代码?但是,这种动力是由惯用代码编写的现有C程序员承担的。新的C程序员采用这些惯用语是为了编写代码,这将使那些将要审阅其代码的人员更容易理解。

© www.soinside.com 2019 - 2024. All rights reserved.