【问题标题】:Why is iterating through an array backwards faster than forward in C为什么在 C 中向后迭代数组比向前迭代更快
【发布时间】:2023-04-06 17:11:01
【问题描述】:

我正在准备考试并试图解决这个问题: 我有以下 C 代码来做一些数组初始化:

int i, n = 61440;
double x[n];
for(i=0; i < n; i++) {
  x[i] = 1;
}

但以下运行速度更快(1000 次迭代相差 0.5 秒):

int i, n = 61440;
double x[n];
for(i=n-1; i >= 0; i--) {
  x[i] = 1;
}

我首先认为这是由于循环访问 n 变量,因此必须进行更多读取(如此处建议的示例:Why is iterating through an array backwards faster than forwards)。但即使我将第一个循环中的 n 更改为硬编码值,反之亦然,将底部循环中的 0 移动到变量中,性能保持不变。我还尝试将循环更改为只完成一半的工作(从 0 到 = 30720),以消除对 0 值的任何特殊处理,但底部循环仍然更快

我认为这是因为一些编译器优化?但是我查找生成的机器代码的所有内容都表明, = 应该是相等的。

感谢任何提示或建议!谢谢!

编辑:Makefile,用于编译器详细信息(这是多线程练习的一部分,因此是 OpenMP,尽管在这种情况下,它全部在 1 个核心上运行,代码中没有任何 OpenMP 指令)

#CC = gcc

CC = /opt/rh/devtoolset-2/root/usr/bin/gcc
OMP_FLAG = -fopenmp
CFLAGS = -std=c99 -O2 -c ${OMP_FLAG}
LFLAGS = -lm

.SUFFIXES : .o .c

.c.o:
    ${CC} ${CFLAGS} -o $@ $*.c

sblas:sblas.o
    ${CC} ${OMP_FLAG} -o $@ $@.o ${LFLAGS}

Edit2:我用 n * 100 重新进行了实验,得到了相同的结果: 前锋:~170s 向后:~120s 与之前的 1.7s 和 1.2s 的值类似,只是乘以 100

Edit3:最小示例 - 上述所有更改都本地化为矢量更新方法。这是默认的向前版本,比向后版本花费的时间更长for(i = limit - 1; i &gt;= 0; i--)

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <omp.h>

void vector_update(double a[], double b[], double x[], int limit);

/* SBLAS code */

void *main() {
    int n = 1024*60;
    int nsteps = 1000;
    int k;

    double a[n], b[n], x[n];

    double vec_update_start;
    double vec_update_time = 0; 

    for(k = 0; k < nsteps; k++) {
    // Loop over whole program to get reasonable execution time
    // (simulates a time-steping code)
        vec_update_start = omp_get_wtime();
        vector_update(a, b, x, n);
        vec_update_time = vec_update_time + (omp_get_wtime() - vec_update_start);
   }

    printf( "vector update time = %f seconds \n \n", vec_update_time);
}

void vector_update(double a[], double b[], double x[] ,int limit) {
    int i;
    for (i = 0; i < limit; i++ )  {
        x[i] = 0.0;
        a[i] = 3.142;
        b[i] = 3.142;
    }
}

Edit4:CPU 是 AMD 四核 Opteron 8378。机器使用其中的 4 个,但我在主处理器上只使用了一个(AMD 架构中的核心 ID 0)

【问题讨论】:

    标签: c arrays traversal


    【解决方案1】:

    主要原因是您的编译器不太擅长优化。从理论上讲,更好的编译器没有理由不能将您的代码的两个版本都转换为完全相同的机器代码,而不是让一个更慢。

    除此之外的一切都取决于生成的机器代码是什么以及它正在运行什么。这可能包括 RAM 和/或 CPU 速度的差异、缓存行为的差异、硬件预取的差异(和预取器的数量)、指令成本和指令流水线的差异、推测的差异等。请注意(理论上)这并不'不排除(在大多数计算机上但不在您的计算机上)您的编译器为前向循环生成的机器代码比它为向后循环生成的机器代码更快的可能性(您的样本量不足以具有统计意义,除非你在嵌入式系统或游戏机上工作,所有运行代码的计算机都是相同的)。

    【讨论】:

    • 我正在开发一台机器,它保证一旦程序启动,该内核在完成之前不会被中断
    • @CrankMuffler:嗯,很好。它是一个现代 80x86 CPU,其中硬件预取器能够从任一方向检测和预取 3 个流(除了具有“TLB 硬件预取”);或稍旧的 80x86 CPU 或稍微更奇特的 80x86 CPU(例如 Xeon Phi)或根本不是 80x86 或...?
    • @CrankMuffler:我的意思是,要了解低级差异,您需要查看反汇编代码,并且需要知道哪个 CPU(哪个制造商的哪个型号)。通过选择使用高级语言,您选择将所有“微优化决策”委托给第三方(编译器),并调查第三方正在做什么,我们需要的不仅仅是高级源代码。
    • 是的,我明白了 - 只是想说它是可验证的,而不是由随机任务中断进程引起的
    【解决方案2】:

    不是反向迭代,而是与零的比较导致第二种情况下的循环运行得更快。

    for(i=n-1; i >= 0; i--) {
    

    与零的比较可以用一条汇编指令完成,而与任何其他数字的比较需要多条指令。

    【讨论】:

    • 我试图让循环完成一半的工作,所以它不会检查 0 并且它仍然更快:for(i = n-1; i &gt;= 30720; i --) 仍然比 for(i = 0; i &lt; 30720; i++)
    • @CrankMuffler “一半的工作”是指从 n 循环到 n/2 而不是 0 吗?
    • 这是 1980 年代/1990 年代的优化,如今已过时且“未成熟”。编译器现在应该能够生成最有效的代码,而程序员不必显式地编写对零的检查。所以这并不能解释什么。
    猜你喜欢
    • 2012-01-31
    • 1970-01-01
    • 1970-01-01
    • 2012-11-17
    • 1970-01-01
    • 2014-02-22
    • 1970-01-01
    • 2018-11-17
    • 2022-11-22
    相关资源
    最近更新 更多