常量指针数组或数组指针?在C语言中哪个更快?

3

你好,我现在处于一个中间的C类水平,我突然想到了这个问题:

int multi[3][4]; // const multidimensional array

int* ptr = *multi; // ptr is a pointer variable that hold the address of multi const array

所以,访问多维数组位置时,哪种方法更快/更小/更优化呢?
这个:

代码:

multi[3][1]; // index the value position

或者

*(*(multi+2)+1); // pointer to the position on the array

或者(已更新)
ptr += 9; // pointer arithmetic using auxiliary pointer

由于“multi”是一个常量数组,编译器应该已经“知道”元素位置的本地化信息。如果使用指向该数组的变量指针,在寻找要显示的项目时可能需要更多的处理时间,但另一方面,可以更快地搜索所需的项目。哪种方法更快/更小/更优化?

提前感谢您的帮助。


4
你应该更关注代码的可读性。大多数人没有意识到,在C语言中,你不能回答“纳秒”级别的性能比较问题。虽然你可以回答这样的问题,但你必须指定一个实现(或一组实现)。 - Khaled Alshaya
2
即使有性能提升的可能,我也怀疑是否存在,还是选择可读性更好的版本。"过早优化是万恶之源" -Knuth - Clinton Pierce
C语言已经是一个很难处理的语言了,你为什么还想让它更难呢? - David Heffernan
好吧,有人应该对我的教授说这个。他的话是:1. *(ptr+1) 是作弊;2. 你永远不应该使用 arr[row][col];等等。 - Mariz Melo
1
@Maris Melo: 叹气。这是反对使用C作为教学语言的又一个论据;已经形成的民间传说、错误信息和简直糟糕的实践数量惊人。许多学生遇到C语言问题的原因是因为通常教学不好。告诉你的教授编写代码并分析不同版本的性能,他可能会感到惊讶。 - John Bode
因为我就是放不下,所以我写了一个小程序来遍历一个二维数组,一次使用索引,一次使用指针算术,并对其进行了分析。在我的系统上,使用下标比使用指针算术始终更快,速度提高了20%以上。太多人像你的教授一样“知道”巧妙的代码比直接的版本“更快”,但往往他们是错的 - John Bode
5个回答

8
他们都是以同样的方式编译的:*(指向第一个元素的指针 + x + y)

使用常规下标运算符(对于该代码和编译器设置)产生了最少的指令。 - John Bode
@john:我猜我的7个赞中有5个应该是给你的.. ;) 我之前也做过一些关于单维数组的工作,生成的汇编代码也是一样的,所以我认为对于二维数组来说,汇编代码也应该是一样的。 - BlackBear

6

首先

int multi[3][4];

这不是一个 const 数组。在这个声明中没有任何 const

其次,

int* ptr = *multi;

这将使得ptr指向元素multi[0][0]。从数值上来说,它与multi[0]和整个multi的地址相同,但类型是不同的。

第三点,

multi[3][1];

根据定义,它与

是相同的。

*(*(multi + 3) + 1);

因此,性能上不应该有任何合理的差异。它与上述内容的联系并不清楚。

第四点,

*ptr + 9;

它完全没有“指针算术”功能。这相当于

multi[0][0] + 9;

这是一个普通的整数加法。然而,它与上述内容的联系并不清楚。
最后,你的问题标题为“常量指针数组或指向数组的指针”,但在实际问题文本中我没有看到这两个概念。

关于ptr + 9,值得一提的是它不包含指针算术运算,因为一元运算符的优先级高于二元+。如果代码是ptr ++,那么它将包含指针算术运算。++的优先级与相等,但在这种情况下,操作符从右到左进行求值。这些规则并不简单,所以您不应该依赖操作符优先级,而应该在一行上混合多个操作符时使用括号。 - Lundin
这是标准使它变得过于复杂了。其实我不正确,postfix ++的优先级高于*,我把后缀优先级与前缀优先级混淆了。我的观点是,这些运算符优先级规则很难跟踪,所以要使用括号!不要依靠它们,也不要假设程序员知道它们或记住它们。 - Lundin
@Andrey 你关于分组的观点是正确的,尽管它们是运算符优先级规则的一部分。请参考ISO 9899:1999 6.5 $3中的说明:“运算符和操作数的分组由语法指示。74)”,注释74:“语法规定了表达式求值中运算符的优先级,其顺序与本子句的主要子子句的顺序相同,最高优先级在前。” - Lundin
@Lundin: 但这正是我所说的。 语言的“语法”就是它的语法。 语法定义了分组和优先级。 优先级通常是非线性的,这意味着通常无法根据它们的优先级将所有运算符按顺序排列。 这就是为什么您在标准中找不到任何“运算符优先级表”的原因。 - AnT stands with Russia
@Lundin:基本上,这个笔记完整地表达了我一直以上所说的话,逐字逐句。您试图通过向我展示这个笔记来证明什么并不立即清晰。 - AnT stands with Russia
显示剩余6条评论

4
无论你的代码有多快,如果因为没有人能够理解和维护它而被抛弃,那就毫无意义;此外,复杂的代码有时会阻止编译器进行更好的优化。
例如:
int main(void)
{
  int arr[3][4] = {{0,1,2,3},{4,5,6,7},{8,9,10,11}};
  int *p = *arr; // == arr[0] == &arr[0][0]

  int x;

  x = arr[2][3];         // Straightforward array access
  x = *(*(arr+2)+3);     // Ugly pointer arithmetic
  x = *(ptr + 11);       // Slightly less ugly pointer arithmetic

  return 0;
}

我通过 gcc -c -g -Wa,-a,-ad > foo.lst 运行了上述代码,以获取生成的汇编和源代码交错的结果。

以下是 x = arr[2][3]; 的翻译:

movl      -16(%ebp), %eax     
movl      %eax, -8(%ebp)      

这是 x = *(*(arr+2)+3); 的翻译:
leal      -60(%ebp), %eax
addl      $44, %eax
movl      (%eax), %eax
movl      %eax, -8(%ebp)

最后,x = *(ptr + 11);的翻译如下:
movl      -12(%ebp), %eax
addl      $44, %eax
movl      (%eax), %eax
movl      %eax, -8(%ebp)

不要试图比编译器更聪明。现在已经不是1970年代了。gcc知道如何高效地访问数组,而无需您告诉它。

除非您已经调整了算法和数据结构,使用了编译器的最高优化设置(值得一提的是,-O1为所有三个版本生成相同的代码),并且仍然无法满足硬性性能要求(在这种情况下,正确的答案通常是购买更快的硬件),否则您甚至不应该考虑性能问题。在没有通过分析器找到真正瓶颈之前,不要更改任何内容。 测量,不要猜测。

编辑

当文字23被变量替换时,情况会发生变化。在这种情况下,*(ptr + offset);看起来最好。但差别不大。我仍然认为,在这个层面上,清晰度更重要。


2

a[i]的意思是*(a+i),实际上你也可以写成i[a],编译器也会接受。

从理论上讲,*(ptr+i)可能比ptr[j][k]稍微快一点,因为你只进行了一次加法运算(而ptr[j][k]可能需要2次)。


1
a[i] 的意思是 *(a+i) 而不是 (a+i) - Prasoon Saurav

1

我不确定const数组是从哪里来的,但为了讨论,让我们假设原帖中有一些。

由于原帖作者没有明确提到PC编程,因此const数组和常规数组不一定以相同的方式编译。

在嵌入式系统中,如果const数组分配在真正的非易失性存储器中,即具有真正ROM的嵌入式应用程序中,const数组可能比非const数组慢。慢速与ROM的访问时间有关,并且高度依赖于硬件。


关于指针算术运算,那是访问数组的唯一方式。在C语言中,数组语法被编译器人员称为“语法糖”,也就是说它只是为了好看而存在。像这样的数组访问:

arr[i]

被编译器转换为

*(arr+i)

它们在性能和功能上是等效的。


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