【问题标题】:Do modern c++ compilers optimize assignments after type casting?现代 c++ 编译器是否在类型转换后优化分配?
【发布时间】:2022-02-08 05:09:04
【问题描述】:

取以下代码:

char chars[4] = {0x5B, 0x5B, 0x5B, 0x5B};
int* b = (int*) &chars[0];

(int*) &chars[0] 值将在循环(长循环)中使用。在我的代码中使用 (int*) &chars[0] 而不是 b 有什么优势吗?创建b 是否有任何开销?由于我只想将其用作别名并提高代码可读性。

另外,只要我知道自己在做什么,就可以进行这种类型转换吗?或者我应该总是memcpy() 到另一个具有正确类型的数组并使用它?我会遇到任何未定义的行为吗?因为在我目前的测试中,它是有效的,但我看到人们不鼓励这种类型转换。

【问题讨论】:

  • 你可能会发现godbolt.org很有用
  • 现代编译器几乎优化了你能想到的一切。如果有帮助,一个临时变量肯定会被优化掉。相反,多次使用的表达式很可能只计算一次并缓存在寄存器中,就好像您已将其分配给临时变量(公共子表达式消除)。因此,无论您编写哪种方式,您都可能会得到相同的代码;因此,只需选择更清晰的。
  • 您的代码中没有分配。只有初始化(=s 不是赋值)。无论如何,当您深入到机器指令级别时,初始化变量的简单类型转换(如您的示例中所示)都是免费操作。在您的情况下,编译器可以识别 &chars[0] 是一个有效的内存地址,并且通常只会使用相同的值初始化 b (类型对编译器很重要,而不是机器指令)。使用这些变量的后续代码可能具有未定义的行为。 [此外,测试不能证明不存在未定义的行为]。
  • 顺便说一句,在这个例子中,你为什么还要费心从数组中转换呢? int x = 0x5b5b5b5b; int *b = &x; 更短,避免了严格的别名问题,而且这个值甚至与字节序无关。
  • 我不确定演员表是否有严格的别名问题,但如果确实如此,那么memcpy 会修复它,并且一个体面的编译器也可以优化 memcpy,所以它不会有额外的费用。在 C++20 中,可能有一种方法可以用 bit_cast 代替。

标签: c++ optimization strict-aliasing


【解决方案1】:

只要我知道自己在做什么就可以进行这种类型转换吗?

不,这不行。 这不安全。 C++ 标准不允许这样做。尽管结果取决于目标平台(由于 endianess 和填充),但您可以访问对象表示(即,将对象指针转换为 char*)。但是,你不能安全地做相反的事情(即没有未定义的行为)。

更具体地说,int 类型与char(未对齐)相比,可能有不同的对齐要求(通常对齐到 4 或 8 个字节)。因此,当 b 将被取消引用时,您的数组可能未对齐并且强制转换会导致未定义的行为。请注意,尽管主流 x86-64 处理器支持它,但它可能会导致某些处理器(例如 AFAIK、POWER)崩溃。此外,编译器可以假设b 在内存中对齐(而不是alignof(int))。

或者我应该总是将 memcpy() 保存到另一个具有正确类型的数组并使用它吗?

是的,或者替代 C++ 操作,如自 C++20 起可用的新 std::bit_cast。不要担心性能问题:大多数编译器(GCC、Clang、ICC,当然还有 MSVC)都会优化此类操作(称为类型双关)。

我会遇到任何未定义的行为吗?

如前所述,可以,只要类型双关语没有正确完成。有关这方面的更多信息,您可以阅读以下链接:

因为到目前为止,在我的测试中,它有效,但我看到人们不鼓励这种类型转换。

它通常适用于 x86-64 处理器上的简单示例。但是,当您处理大代码时,编译器确实会执行愚蠢的优化(但关于 C++ 标准的优化是完全正确的)。引用cppreference:“编译器不需要诊断未定义的行为(尽管诊断了许多简单的情况),并且编译后的程序不需要做任何有意义的事情。”。此类问题很难调试,因为它们通常仅在启用优化时以及在某些特定情况下才会出现。程序的结果可能会因内联函数而改变,这取决于编译器启发式。在您的情况下,这取决于堆栈的对齐方式,这取决于编译器优化和当前范围内声明/使用的变量。某些处理器不支持未对齐的访问(例如,跨越缓存线边界的访问),这会导致数据损坏的硬件异常。

简而言之,到目前为止“它有效”并不意味着它总是随时随地都有效。

(int*) &chars[0] 值将在循环(长循环)中使用。在我的代码中使用 (int*) &chars[0] 优于 b 有什么优势吗?创建 b 是否有任何开销?由于我只想将其用作别名并提高代码可读性。

假设您使用正确的方式进行类型双关语(例如memcpy),那么只要启用优化标志,优化编译器就可以完全优化此初始化。除非您发现生成的代码优化不佳,否则您不必担心这一点。 正确性比性能更重要

【讨论】:

  • 谢天谢地,C++ 中没有对齐要求 - 否则许多 C API 将无法从 C++ 调用
  • @mmomtchev 您是否阅读了答案中的链接? cppreference explicitly state this。这也是编译器在 struct 中添加填充的主要原因(除了性能改进)。有关更多信息,请阅读 C++17 标准的第 6.6.5 节。例如,它明确指出:“一个对象类型对该类型的每个对象都有对齐要求”。
  • C++ 编译器在分配存储时会对齐 - 甚至这也取决于实现。但是当他们访问它时,他们支持对齐和未对齐的数据。否则 C++ 不会与其他任何东西二进制兼容。
  • @mmomtchev 我更仔细地检查了,同样的事情也适用于 C,所以二进制兼容性参数在这里不相关。 This page 更准确地解释了这一点。 C11(第 6.3.2.3 节)标准对此非常清楚:“如果结果指针未正确对齐引用的类型,则行为未定义。”。所以简而言之:这是一个未定义的行为,在C 和C++ 中都可以做到这一点。
  • 这似乎是因为架构根本无法加载/存储未对齐的单词而被添加到 C11 - ARM 是典型的例子。 ARM 编译器有一个__packed 指针属性,在这些情况下会生成两个或更多单独的负载——但这在 C11 中没有指定,它仍然是一个非标准特性。由于 C11 保持向后兼容,未对齐的内存访问仍然适用于它之前工作的架构。这种访问数据的方式在 TCP/IP 网络代码中非常常见,并且在 x86 上的 ntohs/ntohl 函数中使用。它不适用于 ARM。
【解决方案2】:

AFAIK,C 编译器在转换指针时不会插入任何代码 - 这意味着 charsb 都只是内存地址。通常,C++ 编译器应该以与 C 编译器相同的方式编译它——这就是 C++ 具有不同的、更高级的转换语义的原因。

但是你总是可以编译它然后在gdb中反汇编它自己看看。

否则,只要您知道字节顺序问题或在异国平台上可能存在不同的int 大小,您的投射是安全的。

另请参阅此问题:In C, does casting a pointer have overhead?

【讨论】:

  • 即使sizeof(int) 为 4,转换也不安全,通常是由于不同的对齐约束(没有什么可以防止 chars 未对齐)。即使是这样,AFAIK 这对于 C++ 规范来说也是不安全的。
  • 读取未对齐的单词,即使它不是性能方面最快的解决方案,仍然有效。
  • @mmomtchev:它适用于所有现代台式机。有许多嵌入式平台和一些“老式”台式机(例如基于 68000 的 Macintosh)会失败。
【解决方案3】:

如果代码使用从指向需要字对齐的类型的指针派生的指针执行多个离散字节操作,clang 有时会将离散写入替换为如果原始对象针对该字类型对齐则将成功的字写入,但如果对象未按照编译器预期的方式对齐,则会在不支持未对齐访问的系统上失败。

除其他外,这意味着如果将指向 T 的指针转换为指向包含 T 的联合的指针,如果联合包含任何需要比原始类型更严格的对齐方式,即使联合只能通过原始类型的成员访问

【讨论】:

    猜你喜欢
    • 2015-09-08
    • 2018-04-30
    • 1970-01-01
    • 2015-06-16
    • 1970-01-01
    • 2014-04-12
    • 1970-01-01
    • 1970-01-01
    • 2018-04-10
    相关资源
    最近更新 更多