哪一个更适合访问数组?

Posted

技术标签:

【中文标题】哪一个更适合访问数组?【英文标题】:Which one is more optimized for accessing array? 【发布时间】:2013-02-14 17:57:14 【问题描述】:

解决以下练习:

编写三个不同版本的程序来打印元素 我。一个版本应该使用一个范围来管理迭代, 在使用下标的一种情况下,其他两个应该使用普通的 for 循环 在另一个使用指针。在所有三个程序中写入所有 直接输入。也就是说,不要使用类型别名、auto 或 decltype 简化代码。[C++ Primer]

出现了一个问题:这些访问数组的方法中,哪些在速度方面进行了优化?为什么?


我的解决方案:

    Foreach 循环:

    int ia[3][4]=1,2,3,4,5,6,7,8,9,10,11,12;    
    for (int (&i)[4]:ia)        //1st method using for each loop
        for(int j:i)
            cout<<j<<" ";
    

    嵌套for循环:

    for (int i=0;i<3;i++)       //2nd method normal for loop
        for(int j=0;j<4;j++)
            cout<<ia[i][j]<<" ";
    

    使用指针:

    int (*i)[4]=ia;
    for(int t=0;t<3;i++,t++)  //3rd method.  using pointers.
        for(int x=0;x<4;x++)
            cout<<(*i)[x]<<" ";
    

    使用auto

    for(auto &i:ia)             //4th one using auto but I think it is similar to 1st.  
        for(auto j:i)
             cout<<j<<" ";
    

使用clock()进行基准测试

1st: 3.6  (6,4,4,3,2,3) 
2nd: 3.3  (6,3,4,2,3,2)
3rd: 3.1  (4,2,4,2,3,4)
4th: 3.6  (4,2,4,5,3,4)

每个方法模拟1000次:

1st: 2.29375  2nd: 2.17592  3rd: 2.14383  4th: 2.33333
Process returned 0 (0x0)   execution time : 13.568 s

使用的编译器:MingW 3.2 c++11 标志已启用。 IDE:代码块

【问题讨论】:

AFAIK c 编译器足以优化“正常 for 循环”,所以我更喜欢它(为了可读性) 可读性,可读性,可读性... 如果你真的需要知道,看汇编代码。如果你不知道如何解释它,你不需要知道并且应该为了可读性而编写它并相信编译器会做正确的事情。 针对什么进行了优化?速度?物体大小?可读性?工作保障? @Arpit 我怀疑您使用指针的解决方案是本书要求您使用指针时的意图。我认为它应该是这样的: for (int i = 0, * p = &(ia[0][0]); i 【参考方案1】:

我有一些观察和要点,希望您能从中得到答案。

    第四版,正如您自己所说,与第一版基本相同。 auto 可以被认为只是一种编码快捷方式(这当然不完全正确,因为使用 auto 可能会导致获得与您预期不同的类型,从而导致不同的运行时行为。但大多数时候这是真的。)

    您使用指针的解决方案可能不是人们说他们正在使用指针时的意思!一种解决方案可能是这样的:

    for (int i = 0, *p = &(ia[0][0]); i < 3 * 4; ++i, ++p)
        cout << *p << " ";
    

    或使用两个嵌套循环(这可能毫无意义):

    for (int i = 0, *p = &(ia[0][0]); i < 3; ++i)
        for (int j = 0; j < 4; ++j, ++p)
            cout << *p << " ";
    

    从现在开始,我假设这是您编写的指针解决方案。

    在这种微不足道的情况下,绝对会支配你的运行时间的部分是cout。与执行 I/O 相比,记账和检查循环所花费的时间完全可以忽略不计。因此,您使用哪种循环技术并不重要。

    现代编译器非常擅长优化此类普遍存在的任务和访问模式(迭代数组)。因此,所有这些方法都有可能生成完全相同的代码(指针版本可能例外,即我稍后会谈到。)

    大多数这样的代码的性能将更多地取决于内存访问模式,而不是编译器如何准确生成汇编分支指令(以及其余操作)。这是因为如果所需的内存块不在CPU 缓存,从 RAM 中获取这些字节大约需要数百个 CPU 周期(这只是一个大概的数字)的时间。由于所有示例都以完全相同的顺序访问内存,因此它们在内存和缓存方面的行为将相同,并且运行时间大致相同。

    附带说明,这些示例访问内存的方式是访问内存的最佳方式!线性的,连续的,从头到尾。同样,那里的cout 也存在问题,这可能是一个非常复杂的操作,甚至在每次调用时都会调用操作系统,这可能会导致几乎完全删除(驱逐)从CPU 缓存。

    在 32 位系统和程序上,int 和指针的大小通常相等(都是 32 位!)这意味着您是否传递并使用索引值或指针并不重要成数组。然而,在 64 位系统上,指针是 64 位,但 int 通常仍然是 32 位。这表明在 64 位系统和程序上,通常最好使用数组索引而不是指针(甚至迭代器)。

    在这个特定的例子中,这一点都不重要。

    您的代码非常具体和简单,但一般情况下,向编译器提供尽可能多的有关您的代码的信息几乎总是更好的选择。这意味着您必须使用可用的最窄、最具体的设备来完成工作。这反过来意味着对于编译器而言,通用的for 循环(即for (int i = 0; i &lt; n; ++i))比基于范围的for 循环(即for (auto i : v)更糟糕,因为在后一种情况下编译器只知道您将迭代整个范围,而不是超出它或跳出循环或其他东西,而在通用 for 循环情况下,特别是如果您的代码更复杂,编译器不能确保这一点,并且必须插入额外的检查和测试,以确保代码按照 C++ 标准的要求执行。

    在许多(大多数?)情况下,虽然您可能认为性能很重要,但 它并不。而且大多数时候你重写一些东西来获得性能,你并没有获得太多。在大多数情况下,您获得的性能提升值得您维持可读性和可维护性的损失。因此,请正确设计您的代码和数据结构(并牢记性能),但要避免这种“微优化”,因为它几乎总是值得,甚至还会损害代码的质量。

    一般来说,速度方面的性能非常很难推理。理想情况下,您必须使用可靠的科学测量和统计方法,在真实工作条件下使用真实硬件上的真实数据来测量时间。即使测量一段代码运行所花费的时间也不是一件容易的事。衡量性能很难,推理也更难,但如今它是识别瓶颈和优化代码的唯一方法。

希望我已经回答了你的问题。

编辑:我写了一个非常简单的基准测试你想要做什么。 code is here。它是为 Windows 编写的,应该可以在 Visual Studio 2012 上编译(因为基于范围的 for 循环。)这里是计时结果:

Simple iteration (nested loops): min:0.002140, avg:0.002160, max:0.002739
    Simple iteration (one loop): min:0.002140, avg:0.002160, max:0.002625
   Pointer iteration (one loop): min:0.002140, avg:0.002160, max:0.003149
 Range-based for (nested loops): min:0.002140, avg:0.002159, max:0.002862
 Range(const ref)(nested loops): min:0.002140, avg:0.002155, max:0.002906

相关数字是“最小”时间(对于 1000x1000 阵列,每个测试运行超过 2000 次。)如您所见,测试之间绝对没有区别。请注意,您应该打开编译器优化,否则测试 2 将是一场灾难,案例 4 和案例 5 会比案例 1 和案例 3 稍差。

以下是测试代码:

// 1. Simple iteration (nested loops)
unsigned sum = 0;
for (unsigned i = 0; i < gc_Rows; ++i)
    for (unsigned j = 0; j < gc_Cols; ++j)
        sum += g_Data[i][j];

// 2. Simple iteration (one loop)
unsigned sum = 0;
for (unsigned i = 0; i < gc_Rows * gc_Cols; ++i)
    sum += g_Data[i / gc_Cols][i % gc_Cols];

// 3. Pointer iteration (one loop)
unsigned sum = 0;
unsigned * p = &(g_Data[0][0]);
for (unsigned i = 0; i < gc_Rows * gc_Cols; ++i)
    sum += *p++;

// 4. Range-based for (nested loops)
unsigned sum = 0;
for (auto & i : g_Data)
    for (auto j : i)
        sum += j;

// 5. Range(const ref)(nested loops)
unsigned sum = 0;
for (auto const & i : g_Data)
    for (auto const & j : i)
        sum += j;

【讨论】:

第 3 点:我知道这一点,我故意添加了它,否则我将得到所有的零。但是在所有方法中添加它必须具有相同的效果。在大多数情况下,我最快速地使用第三种方法。为什么? 我想奖励你赏金。但你没有回复我的问题,所以我在等待。 @Arpit 这很难说清楚,因为它很大程度上取决于您的基准测试代码。我猜基准时间的差异来自(a)clock() 具有的低分辨率和精度,(b)cout 和控制台 I/O 代码内部的缓冲和刷新差异和/或(c)多任务性质导致中断和其他进程与您的进程间歇性运行并导致时间上的微小差异的操作系统。 @Arpit 如果你想做一个好的基准测试,我建议至少以下步骤: - 使用更高分辨率和更高精度的计时器,例如Windows 上的 QueryPerformanceCounter() 或 Linux 上的 clock_gettime()。或者您可以在 x86 上使用 RDTSC,但它有其自身的问题。 - 运行更长的循环。为数万或数十万个元素运行一个循环。 - 多次运行每个测试(数十次和数百次)并为每个测试取最低时间值。 - 由于当今 CPU 和缓存的复杂架构,一些小东西实际上无法简单或轻松地进行基准测试。如果可以,请阅读程序集。 @Arpit - 绝对不要使用您不完全熟悉的复杂东西,并且可能会出现不可预测的行为,并且您无法控制高度可变的时间,例如基准测试循环中的 I/O,除非您实际上是在对 I/O 部分进行基准测试!【参考方案2】:

影响它的因素很多:

    这取决于编译器 这取决于使用的编译器标志 这取决于使用的计算机

只有一种方法可以知道确切的答案:测量处理大型数组(可能来自随机数生成器)时使用的时间,这与您已经完成的方法相同,只是数组大小应至少为 1000x1000 .

【讨论】:

以上是关于哪一个更适合访问数组?的主要内容,如果未能解决你的问题,请参考以下文章

Fedora与Ubuntu:到底哪一个更适合你?

在 python 中解析 HTML - lxml 或 BeautifulSoup?其中哪一个更适合啥样的目的?

哪一个更适合 jQuery.ajax 调用? .Net Web 服务还是 .ashx?

其中哪一个更适合论坛的 ASP.NET Access 数据库[关闭]

图标字体与SVG,哪一个更适合与颤动一起使用?

LSA 或 BERT 变压器?哪一个更适合用于短句的实时语义相似性和语义聚类? [关闭]