web-dev-qa-db-ja.com

配列構文vsポインター構文およびコード生成?

この本では、 "Cポインタの理解と使用"リチャードリース 85ページに記載されています。

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

vector[i]によって生成されるコードは、*(vector+i)によって生成されるコードとは異なります。表記vector[i]は、位置vectorで始まるマシンコードを生成しますmovesiこの位置からの位置、およびそのコンテンツを使用します。表記*(vector+i)は、位置vectorで始まるマシンコードを生成し、addsiをアドレスに追加してから、そのアドレスの内容を使用します。結果は同じですが、生成されるマシンコードは異なります。ほとんどのプログラマーにとって、この違いはほとんど意味がありません。

ここからの抜粋 を見ることができます。この文章はどういう意味ですか?どの2つのコンパイラーがどのコンテキストで異なるコードを生成しますか?ベースからの「移動」とベースへの「追加」に違いはありますか? GCCでこれを動作させることができませんでした。異なるマシンコードを生成します。

54
Evan Carroll

引用は間違っています。このようなゴミがこの10年でまだ公開されていることはかなり悲劇的です。実際、C標準ではx[y]*(x+y)として定義しています。

ページの後半の左辺値に関する部分も完全にまったく間違っています。

私見、この本を使用する最良の方法は、ごみ箱に入れるか燃やすことです。

94
M.M

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:

Q.E.D.


C標準では、非常に明示的に (C11 n1570 6.5.2.1p2)

  1. 角括弧[]の式が後に続く後置式は、配列オブジェクトの要素の添字による指定です。 添え字演算子[]の定義は、E1[E2](*((E1)+(E2)))と同一であることです。バイナリ+演算子に適用される変換規則のため、E1が配列オブジェクト(同様に、配列オブジェクトの初期要素へのポインター)であり、E2が整数の場合、E1[E2]は、E2E1番目の要素を指定します(ゼロからカウント)。

さらに、as-ifルールがここに適用されます-プログラムの動作が同じ場合、コンパイラはセマンティクスにもかかわらず同じコードを生成できます同じではありませんでした。

33
Antti Haapala

引用された一節はまったく間違っています。式vector[i]*(vector+i)は完全に同一であり、すべての状況下で同一のコードを生成することが期待できます。

vector[i]*(vector+i)は、定義により同一です。これは、Cプログラミング言語の中心的かつ基本的な特性です。有能なCプログラマなら誰でもこれを理解しています。 Understand and Using C Pointersという本の著者は、これを理解する必要があります。 Cコンパイラの作成者なら誰でもこれを理解できます。 2つのフラグメントは偶然ではなく同一のコードを生成しますが、事実上、Cコンパイラーは事実上1つのフォームを他のフォームにほぼ即座に変換するため、コード生成フェーズに到達するまでに、最初に使用された形式。 (Cコンパイラが*(vector+i)とは対照的にvector[i]に対して大幅に異なるコードを生成した場合、私はかなり驚くでしょう。)

そして実際、引用されたテキストはそれ自体と矛盾します。あなたが述べたように、2つの通路

表記vector[i]は、場所vectorで始まるマシンコードを生成し、この場所からi位置を移動し、そのコンテンツを使用します。

そして

表記*(vector+i)は、場所vectorで始まるマシンコードを生成し、iをアドレスに追加してから、そのアドレスの内容を使用します。

基本的に同じことを言う。

彼の言語は、古い C FAQ listquestion6.2 の言語と不気味に似ています:

...コンパイラは、式a[3]を検出すると、「a」の位置から開始し、3つ先に移動し、そこで文字をフェッチするコードを生成します。式p[3]を検出すると、「p」の位置から開始するコードを生成し、そこでポインター値を取得し、ポインターに3を追加して、最後にポイントされた文字を取得します。

しかし、もちろんここでの重要な違いは、aは配列であり、pはポインターであることです。 FAQリストはa[3]*(a+3)ではなく、a[3](または*(a+3))について説明しています。ここで、aは配列です、対p[3](または*(p+3))で、pはポインターです。 (もちろん、配列とポインターが異なるため、これらの2つのケースは異なるコードを生成します。FAQリストが説明するように、ポインター変数からアドレスを取得することは、配列のアドレスを使用することと根本的に異なります。)

19
Steve Summit

規格は、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用に生成されたコードはポインター演算が配列全体にアクセスできるようにします。 utest1では、左辺値式uh [i]とuw [j]の両方が同じ共用体にアクセスしますが、*(u.h + i)と*(u.w + j)について同じに気付くほど洗練されていませんutest2。

6
supercat

Ithink元のテキストが参照している可能性のあるものは、一部のコンパイラが実行する場合と実行しない場合がある最適化です。

例:

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

vs.

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のポインターポストインクリメント命令を使用できる場合もあります。

2番目の場合、「任意の」ポインター算術式で計算されたaddressが同じプロパティを示すことをコンパイラーが確認するのは「難しい」各反復で一定量を単調に進めるしたがって、最適化が見つからず、追加の乗算を使用する各反復で((void*)vector+i*sizeof(int))が計算されない場合があります。この場合、「移動」される(一時的な)ポインターはなく、一時アドレスのみが再計算されます。

ただし、このステートメントはおそらく、すべてのバージョンのすべてのCコンパイラに当てはまるわけではありません。

更新:

上記の例を確認しました。 without少なくともgcc-8.1 x86-64で有効化された最適化により、2番目の(ポインター算術演算)形式よりも多くのコード(2つの追加命令)が生成されるようです最初(配列インデックス)。

参照: https://godbolt.org/g/7DaPHG

ただし、最適化を有効にするとon-O...-O3)生成されるコードは同じ(長さ)になります両方。

6
JimmyB

これを「狭い範囲で」答えてみましょう(「現状のまま」という記述がやや欠けている/不完全である/誤解を招く理由を既に説明している):

どの2つのコンパイラーがどのコンテキストで異なるコードを生成しますか?

「非常に最適化されていない」コンパイラーは、ほぼすべてのコンテキストで異なるコードを生成する可能性があります。解析中にx[y]は1つの式(配列へのインデックス)であり、*(x+y)two式(ポインターに整数を追加し、それを逆参照します)。確かに、これを(解析中であっても)認識して同じように扱うことはそれほど難しくありませんが、単純/高速なコンパイラを書いている場合は、「あまりにも賢い」ことは避けてください。例として:

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()の場合、コンパイラは2つのことを見ます:最初に(「何かまだ」の)間接参照と、次にポインター/配列に整数を追加します。最適化されていないため、次のようになります。

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

明らかに、これは非常に実装依存であり、一部のコンパイラはf()に使用されるような複雑な命令の使用を嫌うかもしれません(複雑な命令を使用するとコンパイラのデバッグが難しくなります)、CPUにはそのような複雑な命令がないかもしれません.

ベースからの「移動」とベースへの「追加」に違いはありますか?

この本の説明は、おそらく十分な言葉ではありません。しかし、著者は上記の区別を説明したかったと思います-インデックス付け(ベースから「移動」)は1つの表現であり、「追加してから間接参照」は2つの表現です。

これは、コンパイラの実装notの言語定義についてであり、この区別は本で明示的に示されるべきでした。

3

私はいくつかのコンパイラのバリエーションについてコードをテストしましたが、それらのほとんどは両方の命令に対して同じアセンブリコードを提供します(最適化なしでx86でテストされました)。おもしろいのは、gcc 4.4.7がまさにあなたが言ったことを正確に行うことです:例:

C-Code

Assembly code

ARMやMIPSのような他の言語も時々同じことをしていますが、私はすべてをテストしませんでした。そのため、それらは違いがあったようですが、gccの後のバージョンはこのバグを「修正」しました。

2
RoQuOTriX