编译器能否优化变量以使用少于一个字节的空间?

Posted

技术标签:

【中文标题】编译器能否优化变量以使用少于一个字节的空间?【英文标题】:Can compilers ever optimize variables to use less than a byte of space? 【发布时间】:2020-01-24 19:26:15 【问题描述】:

我正在考虑针对枚举类型存储在数组或哈希表中的情况进行真正精细的代码优化。

我有一个动物枚举数组,其中每个枚举有 4 个可以表示的不同类别,CatDogFishBird(是否有一个类别的名称顺便说一句,枚举类型?)。我需要检查某个范围内的每个值是否相同。这是执行此操作的未优化方式:

func same([]Animal data, int start, int end) -> Animal or None:
    if (end - start < 0 || end > data.end || start < data.start):  // Bounds checks
        return None
    else if (start == end):                                        // Trivial case
        return data[min_index]
    else:                                                          // Meat & potatoes
        first_value = data[min_index]
        relevant_data = data[min_index:max_index]                      // Slice of relevant parts
        for (value in relevant_data):                                  // Iterate over relevant data
            if value != first_value:                                       // Check if same
                return None                                                // Reject on first non-match
        return first_value                                             // Accept on base case

现在这很好,对于最坏和平均情况,它的时间复杂度为 O(n),但它每次都涉及到讨厌的 if,我认为这可能会在编译器级别存在分支错误预测的风险。更优雅的做法是以不同的方式存储 data,而不是将其隐式存储为:

Animal = Enum(
    Cat       // 0b00
    Dog       // 0b01
    Fish      // 0b10
    Bird      // 0b11
)

我们可以将数据改为这样存储:

Animal = SuperSpecialEnum(
    Cat       // 0b0001
    Dog       // 0b0010
    Fish      // 0b0100
    Bird      // 0b1000
)

那么,我们可以改用这段代码:

func same([]Animal data, int start, int end) -> Animal or None:
    if (end - start < 0 || end > data.end || start < data.start):  // Bounds checks
        return None
    else if (start == end):                                        // Trivial case
        return data[min_index]
    else:                                                          // Thanksgiving
        first_value = data[min_index]
        relevant_data = data[min_index:max_index]                      // Slice of relevant parts

        for (value in relevant_data):                                  // Iterate over relevant data
            first_value &= value                                           // Bitwise and check

        if (first_value == 0b0000):
            return None                                                // Reject on non-match case
        else:
            return first_value                                         // Accept on base case

现在,由于按位first_value &amp;= value,我们可以完全避免分支。当然,我们放弃了早期拒绝案例,这本来可以拯救我们,这实际上有点难说,我认为您需要考虑任何给定值不同的总体概率的二项分布。但是好处是您完全消除了分支,并且许多现代编译器和架构支持 128 位 and 操作,所以这个操作可能真的非常快 .对于最坏和平均情况,您仍然是 O(n) 时间复杂度,但您可能会使用 128 位布尔算法将迭代次数减少 16 倍(16 值 and,假设编译器知道它正在做什么,而且通常都会这样做),并且完全消除了分支错误预测的风险。

现在真正的问题是双重的,尽管这两个问题实际上是编译器是否优化子字节值以使用更少空间的同一问题的不同应用。 一个,假设枚举类别计数恒定(仍然感兴趣顺便说一下,如果这些类别有合适的名称)。 ,如果编译器知道您只使用每个 Animal 的前 4 位从而允许 32 值 and 和 128 位布尔值,您是否可能获得 32 倍的加速算术?

【问题讨论】:

术语旁注:枚举的代表一组有限的alternativescases。大多数类型都有很多值,例如字符串(逻辑上)是无限的。枚举类型很特殊,因为它们的值集非常有限。 两点:(1)您可以通过编写1 &lt;&lt; x而不是x来模拟第一种情况下第二个示例的行为,其中x是紧凑存储的值。这可能会为您提供两全其美的效果,因为如果变量存储在比最小可寻址内存单元(即一个字节)更小的单元中,则无论如何您都需要使用位移位来访问它们。 (2) 带有if 语句的第一个版本可能更有效,因为当它发现一个不同的元素时它会短路。第二个版本总是遍历整个列表,它不会短路。 【参考方案1】:

大多数架构仅通过 SIMD 支持 128 位 AND,在这种情况下,它们通常还支持压缩字节 / int16 / int32 上的相等比较。例如x86 pcmpeqb/w/d/q.

您可以将一些比较结果合并在一起,并在(高速缓存行或整个数组的)末尾检查 SIMD 向量的每个元素都有一个“真”,即数组的每个元素都与第一个元素匹配。

您可以在打包的 2 位字段上执行此操作,在以某种方式进行位广播以将第一个 2 位字段复制到一个字节中的所有其他对,并将其字节广播到整个 SIMD 向量(如 AVX2 vpbroadcastb,或在 C 内在函数中 _mm_set1_epi8)。对于打包的 2 位或 4 位字段,比较相等性仍然适用于这种情况,尽管每个 16 字节向量可能需要几个额外的前端 uops 来加载 + pcmpeqb 和另一个 reg + pand,而 @987654328 @ 带有内存源操作数。尽管如此,在大多数 CPU 上允许两倍密集的表示形式弥补了这一点,尤其是当您考虑将缓存占用空间减半时。


我认为任何编译器都不会为您执行此枚举重新编号,或打包枚举数组以存储每个字节的两个半字节元素。

绝对不是任何当前的 C 编译器;该语言(以及该语言的 ABI)对类型宽度和数据布局定义了太多,当数据结构完全为函数私有并完全重构类型的工作方式时,编译器通常不值得花时间寻找罕见的情况.

(此外,编译器编写者通常不值得花时间尝试编写可以安全/正确地进行主要高级转换的代码,这些转换使内存中的数据布局与源所说的不同。优化完全移除一个数组当然已经完成,但改变它的类型并不是我见过的 C 编译器做的事情。)

但可以肯定的是,理论上这可能是可行的,尤其是在与 C 不同的语言中,enum 没有定义 如何 事物是自动编号的。 (在 C 中定义明确:从 0 开始并以 1 递增,除非您像 enum foo a = 1&lt;&lt;0, b = 1&lt;&lt;1, ... ; 那样覆盖它

提前编译的语言将针对为平台定义的 ABI(例如,为 x86-64 GNU/Linux 编译时的 x86-64 System V ABI)。这使得来自不同编译器的代码,以及同一编译器的不同版本/优化设置,可以相互调用。

enum 作为函数 arg 或返回值使其成为 ABI 的一部分,对于非 static 函数(即可以从单独编译的代码调用的函数)。因此,除了链接时优化(和内联)之外,编译器无法选择跨非内联函数边界的数据表示。 (除了有时通过跨程序优化,有时通过跨源文件的链接时优化启用。)

还请记住,C 编译器通常关心是否对低级系统编程有用,包括与硬件交互或能够内存映射文件。如果这种情况发生,那么数据表示就会在外部可见。这就是为什么编译器不想考虑进行数据打包来改变数组存储方式的原因。很难证明(在地址从不转义函数的本地人之外)没有其他人可能关心数组的数据布局。


修改相邻数组元素的原子性/线程安全

C/C++ 保证不同的线程可以修改不同的对象而不会互相干扰。 (因为 C11 / C++11 引入了线程感知内存模型)。

这包括char array[]enum foo array[] 的相邻元素。每个数组元素都是一个单独的对象。 (以及作为整个数组对象的一部分)。 C++ memory model and race conditions on char arrays 和 Can modern x86 hardware not store a single byte to memory?(可以,每个带有字节存储指令的 ISA 也可以)

在单线程语言中,或者没有像这样的保证的语言中,是的,理论上你可以有一个将枚举打包到子字节字段中的实现。

有趣的事实:在 C++ 中,std::vector&lt;bool&gt; 必须是压缩位图模板特化 (useful data structure, unfortunately historical choice of class to expose it through)。这使得不同线程同时执行vbool[1] = falsevbool[2] = false 是不安全的,这与其他任何安全的std::vector 不同。


如果要这个优化,一般要自己写在源码中

【讨论】:

以上是关于编译器能否优化变量以使用少于一个字节的空间?的主要内容,如果未能解决你的问题,请参考以下文章

java 反编译

C++:编译器能否优化按值传递?

编译器会优化和重用变量吗

nginx的优化使用

如何设置 C/C++ 编译器选项以对使用中的 CPU 进行最佳优化? [关闭]

1分钟了解C语言正确使用字节对齐及#pragma pack的方法