【问题标题】:Unexpected optimization of strlen when aliasing 2-d array对二维数组进行别名时 strlen 的意外优化
【发布时间】:2020-03-04 16:12:02
【问题描述】:

这是我的代码:

#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);
}

将 gcc 8.3.0 或 8.2.1 与除 -O0 之外的任何优化级别一起使用,当我期待 2 2 时,这会输出 0 2。编译器决定strlen 绑定到b[0],因此永远不能等于或超过被除的值。

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

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

(额外的问题,是否有一个 gcc 标志来关闭这个特定的优化?)

【问题讨论】:

标签: c multidimensional-array compiler-optimization strlen gcc8


【解决方案1】:

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

然后我切换到-O2,但没有删除-fno-tree-ccp 标志。它再次重现。我说“OK”并开始测试额外的-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 在问题 cmets 中指出的那样,-fno-builtin-strlen 也会导致 inteded 行为,因此很可能存在编译器错误与内置 strlen另一种优化,旨在切断死代码,静态确定可能的表达式值并通过程序传播常量。我认为编译器很可能错误地考虑了某些东西,它决定了其 strlen 实现中的字符串长度(可能与 整数除法 和/或 二维数组结合使用) 作为死代码并在编译时将其截断或计算为 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() a很多。仍然-fno-tree-ccp -fno-tree-vrp 也是一个选项。

【讨论】:

  • 谢谢,有用的信息。在这个阶段,我的感觉是它在 gcc 8) 中是一个不正确的优化(即错误)。但是很高兴有使用这些特定标志的解决方法。
  • @M.M 在阅读了一些 cmets 后编辑了答案。
【解决方案2】:

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

//typedef char BUF[8];

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

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

如果我以这种方式定义它,那么它会以任何一种方式返回预期值。见here

【讨论】:

  • 您的更改使代码从根本上不同。如果您不喜欢 typedef,则使用 char[8] 而不是 BUF 到处重写代码
  • 是的,我同意;它是不同的。只是玩弄不同的 gcc 版本,看起来 gcc 在 v8.x 中对其进行了不正确的优化,但在 v7.x 或 v9.x 中并不在意。我实现它的方式适用于所有三个,并且仍然保留(据我了解)内存映射的意图(23x 8 字节块)。当然,这是一个有趣的发现,而且我学到了一些关于 typedef 数组的新知识。
  • @PiMaker0 我在没有 typedef 的情况下复制了它。看我的回答
  • @PiMaker0 也没有结构。
【解决方案3】:

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

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

在上面的代码中s.b是一个8字符数组的23项数组。当您仅引用 s.b 时,您将获得 23 字节数组中第一个条目的地址(以及 8 字符数组中的第一个字节)。当代码显示&amp;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 版本仍然可以按您的预期工作。对于它的价值,我在 x86_64-pc-linux-gnu 上运行 clang 版本 9.0.0-2 和 gcc 版本 9.2.1)。

【讨论】:

    【解决方案4】:

    我认为这可能是 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

    【讨论】:

    • 你可以用更简洁的方式指定-fno-builtin-strlen,正如@KamilCuk 评论的那样。
    【解决方案5】:

    代码中有错误。

     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),而不是下一个“可用空间”没有对齐...

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

    【讨论】:

      猜你喜欢
      • 2015-08-27
      • 1970-01-01
      • 1970-01-01
      • 2015-09-04
      • 2023-02-08
      • 2013-07-22
      • 2013-08-15
      • 2022-11-29
      • 2018-04-08
      相关资源
      最近更新 更多