簡體   English   中英

數組語法vs指針語法和代碼生成?

[英]Array-syntax vs pointer-syntax and code generation?

理查德·里斯(Richard Reese)所著的“理解和使用C指針”一書在第85頁說,

 int vector[5] = {1, 2, 3, 4, 5}; 

vector[i]生成的代碼與*(vector+i)生成的代碼不同。 標記vector[i]生成從位置矢量開始的機器代碼,從該位置移動 i位置,並使用其內容。 符號*(vector+i)生成從位置vector開始的機器代碼, i 添加到地址,然后使用該地址的內容。 結果相同時,生成的機器代碼不同。 這種差異對大多數程序員而言幾乎沒有意義。

您可以在此處查看摘錄 這是什么意思? 在什么情況下,任何編譯器會為這兩個生成不同的代碼? 從基本位置“移動”與從基本位置“添加”之間有區別嗎? 我無法在GCC上使用它-生成不同的機器代碼。

引用是錯誤的。 悲劇性的是,這種垃圾在這十年中仍然被發表。 實際上,C標准將x[y]定義為*(x+y)

頁面后面有關左值的部分也完全是完全錯誤的。

恕我直言,使用這本書的最好方法是將其放入回收箱或燃燒。

我有2個C文件: ex1.c

% cat ex1.c
#include <stdio.h>

int main (void) {
    int vector[5] = { 1, 2, 3, 4, 5 };
    printf("%d\n", vector[3]);
}

ex2.c

% cat ex2.c
#include <stdio.h>

int main (void) {
    int vector[5] = { 1, 2, 3, 4, 5 };
    printf("%d\n", *(vector + 3));
}

然后將兩者都編譯成匯編,並展示生成的匯編代碼的區別

% gcc -S ex1.c; gcc -S ex2.c; diff -u ex1.s ex2.s
--- ex1.s       2018-07-17 08:19:25.425826813 +0300
+++ ex2.s       2018-07-17 08:19:25.441826756 +0300
@@ -1,4 +1,4 @@
-       .file   "ex1.c"
+       .file   "ex2.c"
        .text
        .section        .rodata
 .LC0:

優質教育


C標准非常明確地指出(C11 n1570 6.5.2.1p2)

  1. 后綴表達式后跟方括號[]的表達式是數組對象元素的下標名稱。 下標運算符[]的定義是E1[E2](*((E1)+(E2))) 由於適用於二進制+運算符的轉換規則,如果E1是數組對象(等效於指向數組對象初始元素的指針),而E2是整數,則E1[E2]指定E2E2個元素E1 (從零開始計數)。

另外,按此規則適用於此處-如果程序的行為相同,則即使語義相同,編譯器也可以生成相同的代碼。

引用的段落是完全錯誤的。 vector[i]*(vector+i)表達式完全相同,可以在所有情況下產生相同的代碼。

表達vector[i]*(vector+i)通過定義相同。 這是C編程語言的核心和基本屬性。 任何有能力的C程序員都理解這一點。 名為“ 理解和使用C指針 ”的書的任何作者都必須理解這一點。 C編譯器的任何作者都會理解這一點。 這兩個片段將並非偶然地生成相同的代碼,但是因為實際上任何C編譯器實際上都會幾乎立即將一種形式轉換為另一種形式,因此在到達其代碼生成階段時,它甚至都不知道最初使用的是哪種形式。 (如果C編譯器為vector[i]生成與*(vector+i)截然不同的代碼,我會感到非常驚訝。)

實際上,引用的文本本身是矛盾的。 如您所述,這兩個段落

標記vector[i]生成從位置vector開始的機器代碼,從該位置移動i位置,並使用其內容。

符號*(vector+i)生成從位置vector開始的機器代碼,將i添加到地址,然后使用該地址的內容。

說基本相同的事情。

他的語言與舊的C FAQ列表的 問題6.2非常相似:

...當編譯器看到表達式a[3] ,它將發出代碼以從位置“ a ”開始,向其移動三個,然后在此處獲取字符。 當它看到表達式p[3] ,它發出代碼以從位置“ p ”開始,在此處獲取指針值,將指針值加3,最后獲取所指向的字符。

但是,當然關鍵的區別在於a是一個數組, p是一個指針 FAQ列表不是在談論a[3]*(a+3) ,而是在談論a[3] (或*(a+3) ),其中a是一個數組,而與p[3] (或*(p+3) ),其中p是指針。 (當然,這兩種情況會生成不同的代碼,因為數組和指針是不同的。正如FAQ列表所述,從指針變量獲取地址與使用數組的地址根本不同。)

認為原始文本可能是指某些編譯器可能會或可能不會執行的優化。

例:

for ( int i = 0; i < 5; i++ ) {
  vector[i] = something;
}

for ( int i = 0; i < 5; i++ ) {
  *(vector+i) = something;
}

在第一種情況下,優化編譯器可能會檢測到數組vector逐個元素迭代,從而生成類似

void* tempPtr = vector;
for ( int i = 0; i < 5; i++ ) {
  *((int*)tempPtr) = something;
  tempPtr += sizeof(int); // _move_ the pointer; simple addition of a constant.
}

它甚至可以在可用的情況下使用目標CPU的指針后遞增指令。

對於第二種情況,使編譯器看到通過某個“任意”指針算術表達式計算出的地址在每次迭代中顯示單調遞增固定量的相同屬性是“困難的”。 因此,在每次使用附加乘法的迭代中,它可能找不到優化並計算((void*)vector+i*sizeof(int)) 在這種情況下,沒有“臨時”指針被“移動”,而是僅重新計算了一個臨時地址。

但是,該語句可能不適用於所有版本的所有C編譯器。

更新:

我檢查了上面的示例。 似乎在沒有啟用優化的情況下 ,至少gcc-8.1 x86-64會為第二種形式(指針-算術)生成比第一種形式(數組索引)更多的代碼(2條額外指令)。

參見: https : //godbolt.org/g/7DaPHG

但是, 啟用任何優化( -O ... -O3 )后,兩者的生成代碼都是相同的(長度)。

arr是數組對象時,標准將arr[i]的行為指定為等效於將arr分解為指針,添加i並取消引用結果。 盡管這些行為在所有標准定義的情況下都是等效的,但是在某些情況下,即使標准確實要求操作者也可以有效地處理動作,結果arrayLvalue[i]*(arrayLvalue+i)可能會有所不同。

例如,給定

char arr[5][5];
union { unsigned short h[4]; unsigned int w[2]; } u;

int atest1(int i, int j)
{
if (arr[1][i])
    arr[0][j]++;
return arr[1][i];
}
int atest2(int i, int j)
{
if (*(arr[1]+i))
    *((arr[0])+j)+=1;
return *(arr[1]+i);
}
int utest1(int i, int j)
{
    if (u.h[i])
        u.w[j]=1;
    return u.h[i];
}
int utest2(int i, int j)
{
    if (*(u.h+i))
        *(u.w+j)=1;
    return *(u.h+i);
}

GCC為test1生成的代碼將假定arr [1] [i]和arr [0] [j]不能為別名,但是為test2生成的代碼將允許指針算術訪問整個數組。在另一方面,gcc將認識到在utest1中,左值表達式uh [i]和uw [j]都訪問相同的並集,但是它不夠復雜,不足以注意到其中的*(u.h + i)和*(u.w + j)相同utest2。

讓我嘗試在狹義上回答這個問題(其他人已經描述了為什么“按原樣”描述有點缺乏/不完整/誤導):

在什么情況下,任何編譯器會為這兩個生成不同的代碼?

一個“不是非常優化”的編譯器可能會在幾乎任何上下文中生成不同的代碼,因為在解析時有一個區別: x[y]是一個表達式(數組的索引),而*(x+y)兩個表達式(將整數添加到指針,然后取消引用)。 當然,識別這一點(即使在解析時)並加以相同也不是很難,但是,如果要編寫一個簡單/快速的編譯器,則應避免在其中添加“太多的技巧”。 舉個例子:

char vector[] = ...;
char f(int i) {
    return vector[i];
}
char g(int i) {
    return *(vector + i);
}

編譯器在解析f()時會看到“索引”,並且可能會生成類似(對於某些類似68000的CPU):

MOVE D0, [A0 + D1] ; A0/vector, D1/i, D0/result of function

OTOH,對於g() ,編譯器看到兩件事:首先是解除引用(“尚待完成的事情”),然后將整數添加到指針/數組,因此並非非常優化,最終可能會導致:

MOVE A1, A0   ; A1/t = A0/vector
ADD A1, D1    ; t += i/D1
MOVE D0, [A1] ; D0/result = *t

顯然,這是非常依賴於實現的,某些編譯器也可能不喜歡使用用於f()復雜指令(使用復雜指令會使調試編譯器更加困難),CPU可能沒有這樣的復雜指令,等等。

從基本位置“移動”與從基本位置“添加”之間有區別嗎?

本書中的描述可能措辭不當。 但是,我認為作者想描述上面顯示的區別-索引(從基數“移出”)是一個表達式,而“加然后解除引用”是兩個表達式。

這是關於編譯器的實現而不是語言的定義,區別也應該在書中明確指出。

我測試了一些編譯器變體的代碼,其中大多數為我提供了兩條指令的相同匯編代碼(未經優化就針對x86進行了測試)。 有趣的是,gcc 4.4.7確實做到了您提到的內容:示例:

C代碼

匯編代碼

諸如ARM或MIPS之類的其他語言有時也做同樣的事情,但是我沒有對所有這些進行測試。 因此,看來它們是有區別的,但是gcc的更高版本“修復”了該錯誤。

這是C語言中使用的示例數組語法。

int a[10] = {1,2,3,4,5,6,7,8,9,10};

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM