LTO常量传播期间调试GCC警告



我们正在使我们的项目GCC兼容。启用LTO后,链接将花费相当长的时间,并显示以下警告:

../src/xenia/base/memory.h: In function ‘copy_and_swap.constprop’:
../src/xenia/base/memory.cc:105: warning: iteration 4611686018427387903 
invokes undefined behavior [-Waggressive-loop-optimizations]
105 |     dest[i] = byte_swap(src[i]);
|
../src/xenia/base/memory.cc:104: note: within this loop
104 |   for (; i < count; ++i) {  // handle residual elements
|
../src/xenia/base/memory.cc:124: warning: iteration 4611686018427387903 
invokes undefined behavior [-Waggressive-loop-optimizations]
124 |     dest[i] = byte_swap(src[i]);
|
../src/xenia/base/memory.cc:123: note: within this loop
123 |   for (; i < count; ++i) {  // handle residual elements
|

这是我们第一次看到这些函数的问题(通常使用MSVC/Cang(。它们包括向量内部函数。

如何调试此问题?如何获得调用GCC的编译时堆栈跟踪正在尝试优化?

编辑:

这是有问题的代码

inline uint32_t byte_swap(uint32_t value) { return __builtin_bswap32(value); }
void copy_and_swap_32_aligned(void* dest_ptr, const void* src_ptr,
size_t count) {
assert_zero(reinterpret_cast<uintptr_t>(dest_ptr) & 0xF);
assert_zero(reinterpret_cast<uintptr_t>(src_ptr) & 0xF);
auto dest = reinterpret_cast<uint32_t*>(dest_ptr);
auto src = reinterpret_cast<const uint32_t*>(src_ptr);
__m128i shufmask =
_mm_set_epi8(0x0C, 0x0D, 0x0E, 0x0F, 0x08, 0x09, 0x0A, 0x0B, 0x04, 0x05,
0x06, 0x07, 0x00, 0x01, 0x02, 0x03);
size_t i;
for (i = 0; i + 4 <= count; i += 4) {
__m128i input = _mm_load_si128(reinterpret_cast<const __m128i*>(&src[i]));
__m128i output = _mm_shuffle_epi8(input, shufmask);
_mm_store_si128(reinterpret_cast<__m128i*>(&dest[i]), output);
}
for (; i < count; ++i) {  // handle residual elements
dest[i] = byte_swap(src[i]);
}
}

没有内部函数的平台不变版本(只在完整的数组和字节交换上单独循环(不会引发gcc警告。

有问题的数组是如何声明的?

gcc编译器通常会根据这样一个概念来推断循环可能执行的次数,即如果标准没有对构造的行为提出任何要求,即使这是因为委员会期望普通的实现都会对其进行相同的处理,那么任何可能的操作都不会比任何其他操作更好或更糟。

例如,考虑代码片段:

unsigned foo[32770];
unsigned mul_mod_65536(unsigned short x, unsigned short y)
{
return (x*y) & 0xFFFFu;
}
void test(unsigned short n)
{
unsigned sum = 0;
for (unsigned short i=32768; i<n; i++)
sum += mul_mod_65536(i, 65535);
if (n < 32770)
foo[n] = sum & 32767;
}

GCC将把test((函数处理成等效于的代码

void test(unsigned short n)
{
foo[n] = 0;
}

这样的转换是一致的:《标准》的作者表示,他们预计普通编译器会扩展语言的语义,在比《标准》规定的更多情况下,对有符号和无符号整数数学进行相同的处理,但他们不需要这样的行为。该标准允许编译器任意偏离常见行为,要么是因为这种偏离在目标平台上是有意义的,要么是编译器作者认为合适的任何其他原因。

你引用的警告通常是gcc用更多"错误"替换有用代码的结果;高效的";但是无用的代码。认为自己很幸运,你得到了这样的"警告";优化";。在某些情况下(比如我上面发布的例子(,gcc会悄悄地更改代码,从而将普通的因果关系定律抛到九霄云外。在大多数平台上,尝试将32769乘以65535除了产生一个可能毫无意义的值之外,没有什么特别的理由会产生任何副作用。在上面的代码中,如果乘法没有任何奇怪的副作用,那么这种乘法可能产生的任何值最终都会被忽略。然而,GCC会注意到,任何大于32769的n值都会导致整数溢出;尽管溢出发生在标准的作者期望普通实现有意义的情况下,但gcc推断,在n超过32769的任何情况下,它都可以认为是没有意义的。

附录:

也许以下代码片段可能有助于在godbolt中使用:

struct foo { int a[2][2]; };
int bump(struct foo *p, unsigned short n)
{
int total=0;
int i=0;
for (i=0; i < n+2; i++)
{
p->a[0][i] += 1;
}
return i;
}
struct foo foo[4];
int test2(unsigned char n)
{
return bump(foo, n+1);
}

尝试更改数组维度,并将调用中的第二个参数替换为不同的常量n或n+1,您会注意到gcc有时会:

  1. 生成能够对整个数组进行索引的代码
  2. 有时会生成静默代码,其行为就像循环范围被限制在一个维度上一样
  3. 有时会产生一个警告并生成代码,这些代码只是从函数的末尾掉到内存中发生的任何代码中,并且
  4. 有时会无声地生成从函数末尾掉到内存中后面的代码

我对C++不够熟悉,不知道你的例子与这类东西相比会如何,但gcc在内部数组维度方面非常激进。至少在C中,它似乎扩展了语义语言,从而允许指针算术运算+在内部数组的边界之外进行操作(该标准的作者几乎可以肯定的是,这是可能的,但没有强制要求,因为当y不小于内部数组的长度时,对arr[x][y]的访问被明确地描述为UB,但它也被明确地定义为等价于*((&arr[x][0]) + y),这意味着后一种构造在相同的情况下将是UB(。

最新更新