别名二维数组时 strlen 的意外优化

Unexpected optimization of strlen when aliasing 2-d array

这是我的代码:

#include <string.h>
#include <stdio.h>

typedef char BUF[8];

typedef struct
{
    BUF b[23];
} S;

S s;

int main()
{
    int n;

    memcpy(&s, "1234567812345678", 17);

    n = strlen((char *)&s.b) / sizeof(BUF);
    printf("%d\n", n);

    n = strlen((char *)&s) / sizeof(BUF);
    printf("%d\n", n);
}

使用除 -O0 以外的任何优化级别的 gcc 8.3.0 或 8.2.1,这会输出 0 2 而我期望 2 2。编译器决定 strlen 受限于 b[0],因此永远不能等于或超过被除以的值。

这是我的代码中的错误还是编译器中的错误?

这在标准中没有明确说明,但我认为指针出处的主流解释是对于任何对象 X,代码 (char *)&X 应该生成一个可以迭代的指针在整个 X 中——即使 X 碰巧有子数组作为内部结构,这个概念也应该成立。

(奖金问题,是否有关闭此特定优化的 gcc 标志?)

代码中有错误。

 memcpy(&s, "1234567812345678", 17);

例如,有风险,即使 s 以 b 开头 应该是:

 memcpy(&s.b, "1234567812345678", 17);

第二个strlen()也有错误

n = strlen((char *)&s) / sizeof(BUF);

例如,应该是:

n = strlen((char *)&s.b) / sizeof(BUF);

字符串s.b,如果复制正确,应该有 17 个字母长。 不确定结构是如何存储在内存中的,如果它们是对齐的。 您是否检查过 s.b 实际上包含复制的 17 个字符?

所以 strlen(s.b) 应该显示 17

printf只显示整数,因为%d是整数,变量n被声明为整数。 sizeof(BUF), 应该是 8

所以 17 除以 8 (17/8) 应该打印 2,因为 n 被声明为整数。 由于 memcpy 用于将数据复制到 s 而不是 s.b,我猜这与内存对齐有关;假设它是一台 64 位计算机,那么一个内存地址上可以有 8 个字符。

例如,假设有人调用了 malloc(1),然后下一个 "free space" 没有对齐...

第二次 strlen 调用显示了正确的数字,因为字符串复制是针对 s 结构而不是 s.b

我看到了一些问题,它们可能会受到编译器决定内存布局方式的影响。

    n = strlen((char *)&s.b) / sizeof(BUF);
    printf("%d\n", n);

在上面的代码中s.b是一个8字符数组的23条目数组。当您仅引用 s.b 时,您将获得 23 字节数组中第一个条目的地址(以及 8 个字符数组中的第一个字节)。当代码显示 &s.b 时,这是在询问数组地址的地址。在幕后,编译器很可能会生成一些本地存储,将数组的地址存储在那里并将本地存储的地址提供给 strlen.

您有 2 种可能的解决方案。他们是:

    n = strlen((char *)s.b) / sizeof(BUF);
    printf("%d\n", n);

    n = strlen((char *)&s.b[0]) / sizeof(BUF);
    printf("%d\n", n);

我也尝试 运行 你的程序并演示了这个问题,但是 clang 和我拥有的任何 -O 选项的 gcc 版本仍然按你预期的那样工作。对于它的价值,我 运行ning clang 版本 9.0.0-2 和 gcc 版本 9.2.1 on x86_64-pc-linux-gnu).

我认为这可能是 gcc 中的错误。我找到了几个解决方案,但最简单的似乎是创建一个具有 noinline 属性的代理函数。那么你不会失去任何其他优化,只是与 strlen 相关的优化。

int  __attribute__ ((noinline)) _strlen(char *x) { return strlen(x); }
#define strlen _strlen

int main(){
    int n;

    memcpy(&s, "1234567812345678", 17);
    n = strlen((char *)&s.b) / sizeof(BUF);
    printf("%d\n", n);

    n = strlen((char *)&s) / sizeof(BUF);
    printf("%d\n", n);
}

您可以在编译器资源管理器中看到输出。 https://godbolt.org/z/U2L9us

起初您定义结构的方式让我感到困惑,因为我认为我从未尝试过创建数组类型。这样做也很危险,因为如果有人试图将它传递给函数,他们可能会认为他们是按值传递,但实际上会按引用传递。不管风格如何,如果我需要创建这样的类型,我会做类似的事情:

//typedef char BUF[8];

//do it this way instead
typedef struct
{
    char x[8];
} BUF;

typedef struct
{
    BUF b[23];
} S;

如果我这样定义它,那么它 returns 无论如何都是期望值。看到了here.

我检查了这个,它在 gcc 8.3 上用 -O1 重现了,所以我刚打开 gcc 优化标志列表 here 并开始试验它们逐个。事实证明,仅禁用 稀疏条件常量传播 -fno-tree-ccp 会使问题消失(哦,幸运的是,如果一个一个地测试没有结果,我计划测试几个标志) .

然后我切换到-O2但没有擦除-fno-tree-ccp标志。它又重现了。我说“好”,然后开始测试额外的 -O2 标志。似乎再次禁用单个 Value Range Propagation 额外导致预期的 2 2 输出。 然后我删除了第一个 -fno-tree-ccp 标志,但它又开始复制了。因此,对于 -O2,您可以指定 -O2 -fno-tree-ccp -fno-tree-vrp 以使您的程序按预期工作。

我没有删除这些标志,而是切换到 -O3。问题没有复现。

因此 gcc 8.3 中的这两种优化技术都导致了这种奇怪的行为(也许他们使用了内部通用的东西):

  • 树上的稀疏条件常量传播
  • 树上的值范围传播

我不是专业人士,无法解释那里发生的事情和原因,也许其他人可以解释。但是可以肯定的是,您可以指定 -fno-tree-ccp -fno-tree-vrp 标志来禁用这些优化技术,以使您的代码按预期工作。

“越努力越幸运” –塞缪尔·戈德温

编辑

正如 @KamilCuk 在问题评论中指出的那样,-fno-builtin-strlen 也会导致预期行为,因此很可能存在 组合的编译器错误内置 strlen 另一个优化 ,旨在切断死代码,静态确定可能的表达式值并通过程序传播常量。我认为编译器很可能错误地考虑了一些东西,它在其 strlen 实现 中确定了字符串长度(可能与 整数除法 and/or二维数组)作为死码,在编译时截掉或者计算为0。所以我决定稍微玩一下代码来检查理论并消除其他可能的错误“参与者”。我得出了这个最小的行为示例,它证实了我的想法:

int main()
{
    // note that "7" - inner arrays size, you can put any other number here
    char b[23][7]; // local variable, no structs, no typedefs
    memcpy(&b[0][0], "12345678123456781234", 21);

    printf("%d\n", strlen(&b[0][0]) / 8); // greater than that "7" !!!
    printf("%d\n", strlen(&b[0][0]) / 7);
    printf("%d\n", strlen(&b[0][0]) / 6); // less than that "7" !!!
    printf("%d\n", strlen(&b[0][0])); // without division
}

0

0

3

20

我认为我们可以认为这是 gcc 中的一个错误。

我认为 -fno-builtin-strlen 是该问题的更好解决方案,因为它单独适用于所有优化级别,并且内置 strlen 似乎不是那么强大的优化技术,特别是如果您的程序没有不要经常使用 strlen()。不过-fno-tree-ccp -fno-tree-vrp也是一个选项。