别名二维数组时 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
也是一个选项。
这是我的代码:
#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
也是一个选项。