微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决在 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/Clang)。它们包括 向量内在函数

我该如何调试这个问题?如何获取调用 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 中,它似乎扩展了语义语言,以允许指针算术运算 + 超出内部数组的边界进行操作(标准的作者几乎肯定希望这是可能的,但是未能授权,因为当 arr[x][y] 不小于内部数组的长度时对 y 的访问被明确表征为 UB,但它也被明确表征为等同于 *((&arr[x][0]) + y),暗示在相同的情况下,后者的结构将是 UB)。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。