我正在开发一个C (C99)程序,它在许多地方对3-D数组进行大量循环。因此,下面的访问模式在代码中是普遍存在的:
for (int i=0; i<i_size, i++) {
for (int j=0; j<j_size, j++) {
for (int k=0; k<k_size, k++) {
...
}
}
}自然,这会使许多代码行变得混乱,并需要大量的复制。所以我想知道使用宏来使它更紧凑是否有意义,就像这样:
#define BEGIN_LOOP_3D(i,j,k,i_size,j_size,k_size) \
for (int i=0; i<(i_size), i++) { \
for (int j=0; j<(j_size), j++) { \
for (int k=0; k<(k_size), k++) { 和
#define END_LOOP_3D }}}一方面,从DRY principle的角度来看,这似乎很棒:它使代码更加紧凑,并允许您将循环的内容缩进一个块而不是三个块。另一方面,引入新语言构造的做法看起来丑陋得可怕,尽管我现在想不出有任何明显的问题,但似乎很容易产生令人担忧的调试噩梦的bug。
那么你是怎么想的:尽管丑陋和潜在的缺点,但紧凑和减少重复是否证明这是合理的?
发布于 2014-08-05 13:55:00
千万不要在宏中放入open或close {}。C程序员不习惯这样做,因此代码变得难以阅读。
在您的情况下,这甚至是完全多余的,您只是不需要它们。如果你做了这样的事情,那就做吧
FOR3D(I, J, K, ISIZE, JSIZE, KSIZE) \
for (size_t I=0; I<ISIIZE, I++) \
for (size_t J=0; J<JSIZE, J++) \
for (size_t K=0; K<KSIZE, K++)不需要终止宏。程序员可以直接放置{}。
此外,上面我使用了size_t作为C中循环索引的正确类型。3D矩阵很容易变大,当你不去想它的时候,int算法就会溢出。
发布于 2014-08-05 14:45:41
最好的方法是使用函数。让编译器担心性能和优化,但是如果你关心的话,你总是可以将函数声明为内联函数。
下面是一个简单的例子:
#include <stdio.h>
#include <stdint.h>
typedef void(*func_t)(int* item_ptr);
void traverse_3D (size_t x,
size_t y,
size_t z,
int array[x][y][z],
func_t function)
{
for(size_t ix=0; ix<x; ix++)
{
for(size_t iy=0; iy<y; iy++)
{
for(size_t iz=0; iz<z; iz++)
{
function(&array[ix][iy][iz]);
}
}
}
}
void fill_up (int* item_ptr) // fill array with some random numbers
{
static uint8_t counter = 0;
*item_ptr = counter;
counter++;
}
void print (int* item_ptr)
{
printf("%d ", *item_ptr);
}
int main()
{
int arr [2][3][4];
traverse_3D(2, 3, 4, arr, fill_up);
traverse_3D(2, 3, 4, arr, print);
}编辑
为了停止所有的猜测,这里有一些来自Windows的基准测试结果。测试是用大小为2040的矩阵完成的。可以从traverse_3D调用fill_up函数,也可以直接从main()中的3级嵌套循环调用该函数。使用QueryPerformanceCounter()进行基准测试。
案例1: gcc -std=c99 -pedantic-errors -Wall
With function, time in us: 255.371402
Without function, time in us: 254.465830案例2:-Wall -O2的gcc -std=c99 -pedantic-errors
With function, time in us: 115.913261
Without function, time in us: 48.599049案例3: gcc内联-std=c99 - -Wall -errors -O2,traverse_3D函数
With function, time in us: 37.732181
Without function, time in us: 37.430324为什么“没有函数”的情况在内联函数的情况下表现得更好,我不知道。我可以注释掉对它的调用,仍然可以得到“无函数”情况下的相同基准测试结果。
然而,结论是,通过适当的优化,性能很可能不是问题。
发布于 2014-08-05 16:15:51
如果这些3D数组很“小”,你可以忽略我。如果你的3D数组很大,但是你不太关心性能,你可以忽略我。如果你赞同(常见但错误的)原则,即编译器是准魔法工具,几乎可以在不考虑输入的情况下产生最佳代码,那么你可以忽略我。
您可能知道有关宏的一般警告,它们如何阻碍调试等,但如果您的3D数组是“大”的(无论这意味着什么),并且您的算法是面向性能的,那么您的策略可能存在您可能没有考虑到的缺点。
首先:如果你在做线性代数,你几乎肯定想要使用专用的线性代数库,比如BLAS,LAPACK等,而不是“滚动你自己的”。OpenBLAS (来自GotoBLAS)将完全抽掉你编写的任何等效代码,可能至少是一个数量级。如果你的矩阵是稀疏的,这是双重正确的;如果你的矩阵是稀疏的和结构化的(比如三对角线),这是三重正确的。
其次:如果您的3D数组表示用于某种模拟(如有限差分法)的笛卡尔网格,并且/或者打算将其提供给任何数值库,那么您绝对不希望将它们表示为C3D数组。相反,您将希望使用1D C数组,并在可能的情况下使用库函数,并在必要的情况下自己执行索引计算(有关详细信息,请参阅this answer )。
第三:如果您确实必须编写自己的三重嵌套循环,则循环的嵌套顺序是一个重要的性能考虑因素。ijk顺序(而不是ikj或kji)的数据访问模式很可能会导致算法的缓存行为不佳,例如,密集矩阵-矩阵乘法就是这种情况。您的编译器也许能够进行一些有限的循环交换(据我所知,icc可以为naive xGEMM生成相当快的代码,但gcc不会)。随着您实现越来越多的三重嵌套循环,您提出的解决方案变得越来越有吸引力,“一个循环顺序适合所有人”策略在所有情况下都能提供合理性能的可能性变得越来越小。
第四:任何在每个维度的全范围内迭代的“一个循环-顺序适合所有人”的策略都不会被平铺,并且可能会表现出较差的性能。
第五(参考另一个我不同意的答案):一般来说,我认为对于任何对象来说,“最好的”数据类型是具有最小大小和最少代数结构的集合,但是如果您决定放纵内心,使用size_t或另一个无符号整数类型作为矩阵索引,您会后悔的。1994年,我用C++编写了我的第一个朴素线性代数库。在过去的8年里,我用C语言写了大约半打,每次,我都开始尝试使用无符号整数,但每次,我都会后悔。我最终决定size_t是用来表示事物的大小的,而矩阵索引并不是任何事物的大小。
第六(参考另一个我不同意的答案):对于深度嵌套的循环,HPC的一个基本规则是避免在最内层的循环中调用函数和分支。在最内层循环中的操作计数很小的情况下,这一点尤其重要。如果您正在执行一些操作,通常情况下,您不希望在其中添加函数调用开销。如果你在那里做成百上千的操作,你可能不关心函数调用/返回的几条指令,因此,它们是可以的。
最后,如果以上这些都不符合您试图实现的内容,那么您提出的建议没有错,但我会仔细考虑Jens关于花括号所说的话。
https://stackoverflow.com/questions/25131574
复制相似问题