Development Tip

배열 구문 대 포인터 구문 및 코드 생성?

yourdevel 2021. 1. 9. 11:00
반응형

배열 구문 대 포인터 구문 및 코드 생성?


Richard Reese의 "Understanding and Using C Pointers"라는 책에서 85쪽에 이렇게 말합니다.

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

에 의해 생성 된 코드 vector[i]에 의해 생성 된 코드와 다르다 *(vector+i). 표기법 vector[i]은 위치 vector에서 시작 하고이 위치에서 위치를 이동 i 하고 해당 내용을 사용 하는 기계어 코드를 생성합니다 . 표기법 *(vector+i)은 location vector에서 시작하는 기계어 코드를 생성 하고 주소에 추가 i 한 다음 해당 주소의 내용을 사용합니다. 결과는 동일하지만 생성 된 기계어 코드는 다릅니다. 이 차이는 대부분의 프로그래머에게 거의 중요하지 않습니다.

여기 에서 발췌 내용을 볼 수 있습니다 . 이 구절은 무엇을 의미합니까? 어떤 맥락에서 어떤 컴파일러가이 둘에 대해 다른 코드를 생성합니까? 기본에서 "이동"과 기본에서 "추가"간에 차이가 있습니까? 나는 이것을 GCC에서 작동시킬 수 없었습니다-다른 기계 코드를 생성합니다.


견적이 잘못되었습니다. 이러한 쓰레기가 이번 10 년 동안 여전히 출판 된 것은 매우 비극적입니다. 사실, C 표준을 정의 x[y]*(x+y).

페이지 뒷부분의 lvalue에 대한 부분도 완전히 잘못되었습니다.

IMHO,이 책을 사용하는 가장 좋은 방법은 재활용 쓰레기통에 버리거나 태우는 것입니다.


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:

QED


C 표준은 (C11 n1570 6.5.2.1p2) 다음과 같이 명시 적으로 명시합니다 .

  1. 접미사 식 뒤에 대괄호로 묶인 식은 []배열 개체의 요소를 첨자로 지정합니다. 첨자 연산자의 정의 []E1[E2]동일하다(*((E1)+(E2))) . 이항 +연산자에 적용되는 변환 규칙으로 인해 if E1가 배열 객체 (동일하게 배열 객체의 초기 요소에 대한 포인터)이고 E2정수인 경우 (0부터 계산) E1[E2]의- E2번째 요소를 지정합니다 E1.

또한, as-if 규칙 이 여기에 적용됩니다. 프로그램의 동작이 동일하면 의미 체계 동일 하지 않더라도 컴파일러는 동일한 코드를 생성 할 수 있습니다 .


인용 된 구절은 아주 잘못되었습니다. 표현식 vector[i]*(vector+i)완벽하게 동일하고 모든 상황에서 동일한 코드를 생성 할 것으로 예상 할 수있다.

표현식 vector[i]*(vector+i)동일한 정의에 의해 . 이것은 C 프로그래밍 언어의 중심이자 기본 속성입니다. 유능한 C 프로그래머라면 누구나 이것을 이해합니다. C 포인터 이해 및 사용 이라는 책의 저자는 이를 이해해야합니다. C 컴파일러의 모든 작성자는 이것을 이해할 것입니다. 두 조각은 우연이 아니라 동일한 코드를 생성하지만 사실상 모든 C 컴파일러는 사실상 한 형식을 다른 형식으로 거의 즉시 변환하므로 코드 생성 단계에 도달 할 때까지 알지 못합니다. 처음에 사용 된 형태. (C 컴파일러가에 vector[i]반대되는 것과 상당히 다른 코드를 생성했다면 꽤 놀랐 을 것 *(vector+i)입니다.)

그리고 실제로 인용 된 텍스트 자체가 모순됩니다. 언급했듯이 두 구절은

표기법 vector[i]은 location vector에서 시작 i하여이 위치 에서 위치 를 이동 하고 해당 콘텐츠를 사용 하는 기계어 코드를 생성합니다 .

표기법 *(vector+i)은 location vector에서 시작하는 기계어 코드를 생성 i하고 주소에 추가 한 다음 해당 주소의 내용을 사용합니다.

기본적으로 같은 말을합니다.

그의 언어는 이전 C FAQ 목록의 6.2 번 질문 과 매우 유사 합니다 .

... 컴파일러가 표현식을 볼 때 a[3]" a" 위치에서 시작하여 3 개를 지나서 문자를 가져 오는 코드를 내 보냅니다. 표현식 p[3]이 보이면 " p" 위치에서 시작 하여 포인터 값을 가져오고 포인터에 3을 더한 다음 마지막으로 가리키는 문자를 가져 오는 코드를 내 보냅니다.

그러나 물론 여기서 중요한 차이점 a은 배열이고 p포인터라는 것 입니다. 자주 묻는 질문 목록이 없습니다에 대한 이야기 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))추가 곱셈을 사용하는 각 반복에서 계산할 수 있습니다 . 이 경우 "이동"되는 (임시) 포인터는 없지만 다시 계산되는 임시 주소 만 있습니다.

However, the statement probably does not universally hold for all C compilers in all versions.

Update:

I checked the above example. It appears that without optimizations enabled at least gcc-8.1 x86-64 generates more code (2 extra instructions) for the second (pointer-arithmethics) form than the first (array index).

See: https://godbolt.org/g/7DaPHG

However, with any optimizations turned on (-O...-O3) the generated code is the same (length) for both.


The Standard specifies the behavior of arr[i] when arr is an array object as being equivalent to decomposing arr to a pointer, adding i, and dereferencing the result. Although the behaviors would be equivalent in all Standard-defined cases, there are some cases where compilers process actions usefully even though the Standard does require it, and the handling of arrayLvalue[i] and *(arrayLvalue+i) may differ as a consequence.

For example, given

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's generated code for test1 will assume that arr[1][i] and arr[0][j] cannot alias, but the generated code for test2 will allow for pointer arithmetic to access the entire array, On the flip side, gcc will recognize that in utest1, lvalue expressions u.h[i] and u.w[j] both access the same union, but it's not sophisticated enough to notice the same about *(u.h+i) and *(u.w+j) in utest2.


Let me try to answer this "in the narrow" (others have already described why the description "as-is" is somewhat lacking/incomplete/misleading):

In what context would any compiler generate different code for those two?

A "not-very-optimizing" compiler might generate different code in just about any context, because, while parsing, there's a difference: x[y] is one expression (index into an array), while *(x+y) are two expressions (add an integer to a pointer, then dereference it). Sure, it's not very hard to recognize this (even while parsing) and treat it the same, but, if you're writing a simple/fast compiler, then you avoid putting "too much smarts into it". As an example:

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

Compiler, while parsing f(), sees the "indexing" and may generate something like (for some 68000-like CPU):

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

OTOH, for g(), compiler sees two things: first a dereference (of "something yet to come") and then the adding of integer to pointer/array, so being not-very-optimizing, it could end up with:

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

Obviously, this is very implementation dependent, some compiler might also dislike using complex instructions as used for f() (using complex instructions makes it harder to debug the compiler), the CPU might not have such complex instructions, etc.

Is there a difference between "move" from base, and "add" to base?

The description in the book is arguably not well-worded. But, I think the author wanted to describe the distinction shown above - indexing ("move" from base) is one expression, while "add and then dereference" are two expressions.

This is about compiler implementation, not language definition, the distinction which should have also been explicitly indicated in the book.


I tested the Code for some compiler variations, most of them give me the same assembly code for both instructions (tested for x86 with no optimization). Interesting is, that the gcc 4.4.7 does exactly, what you mentioned: Example:

C-Code

Assembly code

Other langauges like ARM or MIPS are doing sometimes the same, but I didn`t tested it all. So it seems their was a difference, but later versions of gcc "fixed" this bug.


This is a sample array syntax as used in C.

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

ReferenceURL : https://stackoverflow.com/questions/51373516/array-syntax-vs-pointer-syntax-and-code-generation

반응형