将数组的所有元素初始化为同一个数字

59

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

在这个例子中是三。

他说这种方式比使用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);
}

18
我不确定为什么它应该略微好一些,因为它似乎具有_未定义的行为_。这是为了提高性能吗? - CB Bailey
33
请注意,假设v没有溢出,它最终将变为“0x3000000300000030000003L”。溢出只是这段代码的第一个问题 - 这位老师不应该教授C ++。 - MSalters
42
别担心,你的编译器也不理解它。如果那段代码在特定的机器和编译器上运行成功,只是偶然而已。 - Greg Hewgill
5
这仍然使用for循环,只是使用了较小的循环,有25次迭代而不是100次。如果 sizeof(long) 是8,这种方法可能可行。但它不会比标准方法更快。 - Barmak Shemirani
5
即使一段C++代码片段也是有效的C代码片段,这并不意味着它自动成为C而非C ++;有些情况下,代码在两种语言中都有效,但在每种语言中具有不同的结果(例如,sizeof('a')在C中为sizeof(int),但在C++中为sizeof(char))。 - Justin Time - Reinstate Monica
显示剩余16条评论
8个回答

76

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

C ++有一个专门用于使用值填充数组的函数,我会在包含#include <algorithm>#include <iterator>后使用它:

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

你不应该低估优化编译器对这种代码的处理能力。

如果你想看看编译器是如何处理的,那么Matt Godbolt的Compiler Explorer是一个非常好的工具,只要你愿意学一点汇编语言。正如你从这里所看到的,编译器可以将fill调用优化为12个(和一点)128位存储,并展开任何循环。因为编译器了解目标环境,所以它们可以在不将任何特定于目标的假设编码到源代码中的情况下进行优化。


10
为什么您在Godbolt的示例中将数组声明为extern?这会导致GCC在A未正确对齐的情况下执行大量无关紧要的操作,使您的观点不太明显。为什么不直接删除extern关键字? - Ruslan

67
他假设longshort长四倍(这不是保证;他应该使用int16_t和int64_t)。
他以更长的内存空间(64位)来装载四个短的(16位)值。他通过将位移16个位置来设置这些值。
然后他想把一个short数组视为long数组处理,这样他只需要进行25次循环迭代就可以设置100个16位值,而不是100次。
这是你老师的思路,但正如其他人所说,这种类型转换是未定义的行为。

24
从技术角度而言,这个程序本身是没有问题的。问题出在通过错误类型的指针进行访问时会导致未定义行为。 - eerorika
尽管从技术上讲这是未定义行为,但任何使用16位short和64位long的编译器,如果它不能按预期工作,我都认为是有问题的。 - Lee Daniel Crocker
20
当然,你可以考虑任何你想要的事情。但是规范就是规范,当涉及到未定义行为时,编译器经常会执行一些可能令你感到惊讶或出错的操作。 - amalloy
4
我很确定GCC会根据严格别名规则作出假设,而这些假设在代码存在时会被打破。实际上,几乎任何有优化野心的编译器都会这样。 - Martin Bonner supports Monica
1
我在这里使用“应该”一词来表示“我认为是正确的”,没有更多,也没有更少。正如我一再强调的那样,我理解规范说明行为是未定义的。这意味着它可以做任何事情。我个人希望它能够做到这一点 - 这只是我的观点,没有更多。我不为我的观点道歉。 - Lee Daniel Crocker
显示剩余2条评论

44

这些都是胡言乱语。

  1. 首先,v 将在 编译时 计算。

  2. 在执行 long *B = (long*)A; 后解引用 B 的行为是未定义的,因为这两种类型无关。 B[i] 是对 B 的解引用操作。

  3. 完全没有理由认为一个 long 比一个 short 大四倍。

以简单方式使用 for 循环,并信任编译器进行优化。拜托啦,加上糖吧。


4
技术上讲,产生未定义行为的不是创建 B 的强制类型转换,而是对其进行访问。 - Martin Bonner supports Monica
@MartinBonner:一如既往地正确,这些事情很重要。我已经修改了。 - Bathsheba

21

这个问题有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网站尝试其他编译器和配置。 另请参阅最新的cppCon演讲


3
+1 对于"So the bottom line [...]"。作为一个不是初学者也不是专业人士的人,OP的代码非常难以理解。如果它确实有效(看到其他答案后),它可能作为有趣的事实,但这条信息很难理解,不能将其作为“另一种方法”进行推广,而应该明确将其作为“有趣的事实”。 - a concerned citizen

7

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

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

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,然后向左移动两个字节(我的修正版本应该更清楚地说明这些神奇数字来自哪里),那么你将得到一个整数,其中包含构成值s的字节的两个副本。重复此操作,直到l中的所有字节对都设置为这些相同的值。要执行移位,您需要移位运算符。

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

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

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


1
这真的是这样吗?严格来说,我认为不能保证 long 的大小是 short 大小的倍数。此外,longs 可能以小端存储,而 longs 是大端(这很疯狂,但是...)。像这样复制底层位可能会导致 short 值的陷阱表示。因此,我认为这种方法仍然是未定义行为。也许安全的方法是使用 short int a[4] 而不是 long - chi
1
@chi 很好的建议。可以使用固定大小的整数,当它们不可用时会安全地失败编译。你的建议也很安全,在所有神秘系统上都能工作。数组可能应该加上 alignas。你的建议给了我一个(可能傻)的想法:将两个值写入输出数组,然后复制这两个值,然后复制四个... 这样会给 memcpy 带来相当少的调用(log N),同时避免设置大的“源”数组。这个循环必须在编译时扩展,以便有希望变得更有效率。 - eerorika

6
您的老师展示给您的代码是一个不规范的程序,无需诊断,因为它违反了指针实际指向所声称指向的内容的要求(也称为“严格别名”)。
以具体的例子来说,编译器可以分析您的程序,注意到未直接写入A并且未写入short,证明A在创建后从未更改过。
根据C++标准,所有与B相关的操作都可以被证明不能修改A。
一个for(;;)循环甚至是一个范围-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,您可以看到该数组的值是在编译时计算出来的,并且当我访问第50个元素时提取了值3。

然而,这有些过度(和)。


2
我认为他试图通过同时复制多个数组元素来减少循环迭代次数。正如其他用户已经在这里提到的,这种逻辑会导致未定义的行为。
如果只是为了减少迭代次数,那么通过循环展开,我们可以减少迭代次数。但对于这样较小的数组来说,速度不会显着提升。
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);
}

5
优化的重点不仅仅是循环展开。它还可以减少拷贝次数(写入单个64位数字比写入4个16位数字的计数更快),而你的建议并没有做到这一点。话虽如此,你的建议至少不像问题中的那个有漏洞。 - eerorika
是的,我的建议并不试图减少复制操作的数量。 - nyemul
1
@user2079303说:“写入一个64位数字比写入4个16位数字的计数更快”,这也取决于目标设备。在一个16位目标设备上,你将需要进行4次复制操作才能完成一个64位的复制。 - CodeMonkey

0
你可以使用 std::array 代替内置数组,并配合一个 make_array 函数模板,该函数将返回一个所有元素均设置为 3(或其他给定数字)的 std::array,如下所示。
template<std::size_t N> std::array<int, N> constexpr make_array(int val)
{
    std::array<int, N> tempArray{};
    for(int &elem:tempArray)
    {
        elem = val;
    }
    return tempArray;
}
int main()
{
    //-------------------------------V-------->number of elements  
    constexpr auto arr  = make_array<8>(5);
    //-------------------------------^---->value of element to be initialized with

    
    //lets confirm if all objects have the expected value 
    for(const auto &elem: arr)
    {
        std::cout << elem << std::endl; //prints all 5 
    }
    
}

工作演示


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接