【问题标题】:Initialize all the elements of an array to the same number将数组的所有元素初始化为相同的数字
【发布时间】:2018-03-17 22:49:08
【问题描述】:

前段时间,我的老老师发布了这段代码,说这是将数组初始化为相同数字(当然不是零)的另一种方法。

在这种情况下是三个。

他说这种方式比for循环稍微好一点。为什么我需要左移运算符?为什么我需要另一个长数组? 我不明白这里发生了什么。

int main() {

    short int A[100];

    long int v = 3;
    v = (v << 16) + 3;
    v = (v << 16) + 3;
    v = (v << 16) + 3;
    long *B = (long*)A;

    for(int i=0; i<25; i++)
        B[i] = v;

    cout << endl;
    print(A,100);
}

【问题讨论】:

  • 我不知道为什么它应该稍微好一点,因为它似乎有未定义的行为。这应该是性能增强吗?
  • 请注意,假设它没有首先溢出,v 以0x3000000300000030000003L 结尾。溢出只是这段代码的第一个问题——这位老师应该在教 C++。
  • 别担心,你的编译器也不理解它。如果该代码在具有特定编译器的特定机器上工作,那只是偶然。
  • 这仍然使用 for 循环,它只是使用了一个较小的循环,有 25 次迭代而不是 100 次迭代。如果sizeof(long) 为 8,它可能会起作用。但它不会比标准方法快。
  • @CiaPan 仅仅因为 C++ sn-p 也是一个有效的 C sn-p,它不会自动使其成为 C 而不是 C++;在某些情况下,代码在两种语言中都有效,但在每种语言中都有不同的结果(例如,sizeof('a') 在 C 中是 sizeof(int),但在 C++ 中是 sizeof(char))。

标签: c++ arrays for-loop


【解决方案1】:

有很多方法可以用相同的值填充数组,如果您关心性能,则需要进行测量。

C++ 有一个专门的函数可以用一个值填充数组,我会使用它(在#include &lt;algorithm&gt;#include &lt;iterator&gt; 之后):

std::fill(std::begin(A), std::end(A), 3);

你不应该低估优化编译器可以用这样的东西做些什么。

如果您有兴趣了解编译器的功能,那么如果您准备学习一点汇编程序,那么 Matt Godbolt 的 Compiler Explorer 是一个非常好的工具。正如您从here 中看到的那样,编译器可以优化fill 对十二个(以及一点)128 位存储的调用,并展开任何循环。由于编译器了解目标环境,因此无需在源代码中编码任何特定于目标的假设即可做到这一点。

【讨论】:

  • 为什么在 Godbolt 的示例中创建数组 extern?这导致 GCC 在A 没有正确对齐的情况下做了很多不相关的事情,这让你的观点不是很明显。为什么不直接删除 extern 关键字?
【解决方案2】:

他假设longshort 长四倍(不能保证;他应该使用 int16_t 和 int64_t)。

他占用了更长的内存空间(64 位)并用四个短的(16 位)值填充它。他通过将位移动 16 个空格来设置值。

然后他想把一个 short 数组当作一个 long 数组,所以他可以通过只做 25 次循环迭代而不是 100 次来设置 100 个 16 位值。

这是你老师的想法,但正如其他人所说,这种演员表是未定义的行为。

【讨论】:

  • 从技术上讲,演员阵容本身就很好。是通过错误类型的指针访问有UB。
  • 虽然从技术上讲这是 UB,但任何使用 16 位 short 和 64 位 long 的编译器都不能完全按预期工作,我会认为它坏了。
  • @LeeDanielCrocker 当然,您可以考虑任何您想要的。但是规范就是规范,编译器经常会做一些你可能会在未定义行为时认为令人惊讶或破坏的事情。
  • @LeeDanielCrocker 我很确定 GCC 会根据严格的别名规则做出假设,这些规则会在出现此代码时中断。事实上,几乎所有以优化为目标的编译器都会这样做。
  • 我在这里用“应该”来表示“我认为是正确的”,仅此而已。正如我一再说过的,我理解规范说行为是未定义的。这意味着它可以做任何事情。我个人更喜欢它做的是——这是我的意见,仅此而已。我不会为我的意见道歉。
【解决方案3】:

真是一派胡言。

  1. 对于初学者,v 将在编译时计算

  2. long *B = (long*)A; 之后取消引用 B 的行为未定义,因为类型不相关。 B[i]B 的取消引用。

  3. 没有任何理由可以假设 longshort 的四倍。

以简单的方式使用for 循环并相信编译器会进行优化。很好,上面加糖。

【讨论】:

  • 从技术上讲,创建 B 的演员不是未定义的行为,而是通过它的访问。
  • @MartinBonner:正确(一如既往;-)),这些事情很重要。我已经修改了。
【解决方案4】:

问题有C++标签(没有C标签),所以应该用C++风格来做:

// C++ 03
std::vector<int> tab(100, 3);

// C++ 11
auto tab = std::vector<int>(100, 3);
auto tab2 = std::array<int, 100>{};
tab2.fill(3);

另外,老师正在尝试超越编译器,它可以做令人兴奋的事情。这样做没有意义,因为如果配置正确,编译器可以为您完成:

如您所见,每个版本的-O2 结果代码(几乎)相同。在-O1 的情况下,技巧会有所改进。

所以归根结底,你必须做出选择:

  • 编写难以阅读的代码,不要使用编译器优化
  • 编写可读代码并使用-O2

使用 Godbolt 站点来试验其他编译器和配置。 另见the latest cppCon talk

【讨论】:

  • +1 表示 所以底线 [...]。作为不是初学者,不是专业人士,OP的代码很难理解。它可能是一个有趣的事实(如果确实如此,请查看其他答案),但该信息是模糊的,不应该被宣传为“另一种方法”,而是明确地作为一个“有趣的事实”。跨度>
【解决方案5】:

正如其他答案所解释的,该代码违反了类型别名规则并做出了标准不保证的假设。

如果您真的想手动进行此优化,这将是一种具有明确行为的正确方法:

long v;
for(int i=0; i < sizeof v / sizeof *A; i++) {
    v = (v << sizeof *A * CHAR_BIT) + 3;
}

for(int i=0; i < sizeof A / sizeof v; i++) {
    std:memcpy(A + i * sizeof v, &v, sizeof v);
}

使用sizeof 修复了关于对象大小的不安全假设,使用std::memcpy 修复了别名冲突,无论基础类型如何,它都具有明确定义的行为。

也就是说,最好让您的代码保持简单,让编译器发挥它的魔力。


为什么我需要左移运算符?

重点是用较小整数的多个副本填充较大的整数。如果您将两个字节的值 s 写入一个大整数 l,然后 shift 剩下两个字节的位(我的固定版本应该更清楚这些幻数的来源)然后您将拥有一个整数,其中包含构成值s 的两个字节副本。重复此操作,直到 l 中的所有字节对都设置为相同的值。要进行班次,您需要班次运算符。

当这些值被复制到包含两字节整数数组的数组上时,单个副本会将多个对象的值设置为较大对象的字节值。由于每对字节具有相同的值,因此数组中较小的整数也将具有相同的值。

为什么我需要另一个 long 数组?

没有long 的数组。只有short 的数组。

【讨论】:

  • 真的是这样吗?严格来说,我认为并不能保证多头的大小是空头大小的倍数。此外,long 可能存储为小端,而 long 是长端(这很疯狂,但是......)。像这样复制底层位可能会导致短值的陷阱表示。所以,我觉得这个方法还是UB。也许安全的方法是使用short int a[4] 而不是long
  • @chi 好点。可以使用固定大小的整数,当它们不可用时,它们会安全地编译失败​​。您的建议也将是安全的,并且适用于所有深奥的系统。数组可能应该用alignas 扩充。您的建议给了我一个(可能很愚蠢)的想法:将两个值写入输出数组,然后复制这两个值,然后复制四个 ... 这将很少调用 memcpy (log N),同时避免设置一个大的“源”数组。这个循环必须延长编译时间,但有希望提高效率
【解决方案6】:

你的老师给你看的代码是一个格式错误的程序,不需要诊断,因为它违反了指针实际指向它们声称指向的东西的要求(也称为“严格别名”)。

作为一个具体的例子,编译器可以分析您的程序,注意到A 没有被直接写入,也没有short 被写入,并证明A 在创建后从未更改。

在 C++ 标准下,所有与 B 相关的问题都可以证明是无法在格式良好的程序中修改 A

for(;;) 循环甚至是 ranged-for 都可能被优化为 A 的静态初始化。你老师的代码,在优化编译器下,将优化为未定义的行为。

如果你真的需要一种方法来创建一个用一个值初始化的数组,你可以使用这个:

template<std::size_t...Is>
auto index_over(std::index_sequence<Is...>) {
  return [](auto&&f)->decltype(auto) {
    return f( std::integral_constant<std::size_t, Is>{}... );
  };
}
template<std::size_t N>
auto index_upto(std::integral_constant<std::size_t, N> ={})
{
  return index_over( std::make_index_sequence<N>{} );
}
template<class T, std::size_t N, T value>
std::array<T, N> make_filled_array() {
  return index_upto<N>()( [](auto...Is)->std::array<T,N>{
    return {{ (void(Is),value)... }};
  });
}

现在:

int main() {

  auto A = make_filled_array<short, 100, 3>();

  std::cout << "\n";
  print(A.data(),100);
}

在编译时创建填充数组,不涉及循环。

使用godbolt,您可以看到数组的值是在编译时计算的,而值 3 是在我访问第 50 个元素时提取的。

然而,这太过分了(和)。

【讨论】:

    【解决方案7】:

    我认为他试图通过同时复制多个数组元素来减少循环迭代的次数。正如其他用户在这里已经提到的那样,这种逻辑会导致未定义的行为。

    如果只是为了减少迭代次数,那么通过循环展开我们可以减少迭代次数。但是对于这么小的数组,它不会明显更快。

    int main() {
    
        short int A[100];
    
        for(int i=0; i<100; i+=4)
        {
            A[i] = 3;
            A[i + 1] = 3;
            A[i + 2] = 3;
            A[i + 3] = 3;
        }
        print(A, 100);
    }
    

    【讨论】:

    • 优化的重点不是只是循环展开。它还减少了副本的数量(写入单个 64 位数字比写入 4 个 16 位数字的计数更快),您的建议没有这样做。也就是说,您的建议至少不像问题中的那样有问题。
    • 是的,我的建议不会尝试减少复制操作的次数。
    • @user2079303 “写入单个 64 位数字比写入 4 个 16 位数字更快”,这也取决于目标。在 16 位目标上,您最终会为一个 64 位副本执行 4 次复制操作。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-17
    • 1970-01-01
    • 2016-12-09
    • 2017-05-11
    • 1970-01-01
    相关资源
    最近更新 更多