왜 GCC가 배열의 초기화를 초기화하여 0이 아닌 요소를 포함하여 모든 것을 먼저 0으로 채우는가?


21

왜 gcc는 나머지 96 개의 정수 대신에 전체 배열을 0으로 채우는가? 0이 아닌 이니셜 라이저는 모두 배열의 시작 부분에 있습니다.

void *sink;
void bar() {
    int a[100]{1,2,3,4};
    sink = a;             // a escapes the function
    asm("":::"memory");   // and compiler memory barrier
    // forces the compiler to materialize a[] in memory instead of optimizing away
}

MinGW8.1과 gcc9.2는 모두 asm을 만듭니다 ( Godbolt 컴파일러 탐색기 ).

# gcc9.2 -O3 -m32 -mno-sse
bar():
    push    edi                       # save call-preserved EDI which rep stos uses
    xor     eax, eax                  # eax=0
    mov     ecx, 100                  # repeat-count = 100
    sub     esp, 400                  # reserve 400 bytes on the stack
    mov     edi, esp                  # dst for rep stos
        mov     DWORD PTR sink, esp       # sink = a
    rep stosd                         # memset(a, 0, 400) 

    mov     DWORD PTR [esp], 1        # then store the non-zero initializers
    mov     DWORD PTR [esp+4], 2      # over the zeroed part of the array
    mov     DWORD PTR [esp+8], 3
    mov     DWORD PTR [esp+12], 4
 # memory barrier empty asm statement is here.

    add     esp, 400                  # cleanup the stack
    pop     edi                       # and restore caller's EDI
    ret

(SSE를 사용하면 movdqa로드 / 저장과 함께 4 개의 이니셜 라이저를 모두 복사합니다)

왜 GCC가 Clang처럼 마지막 96 개 요소 lea edi, [esp+16]만하고 memset을 사용 rep stosd하지 않습니까? 이것이 누락 된 최적화입니까, 아니면이 방법으로 더 효율적입니까? (Clang은 실제로 memset인라인 대신 호출 rep stos)


편집자 주 :이 질문에는 원래 최적화되지 않은 컴파일러 출력이 있었지만 같은 방식으로 작동했지만 비효율적 인 코드는 -O0아무 것도 증명하지 못했습니다. 그러나이 최적화는 에서조차도 GCC에 의해 누락되었습니다 -O3.

a인라인이 아닌 함수에 대한 포인터를 전달하는 것은 컴파일러가 구체화하는 또 다른 방법 a[]이지만 32 비트 코드에서 asm의 심각한 혼란을 초래합니다. 스택 인수로 인해 푸시가 발생하여 스택과 상점이 혼합되어 배열을 초기화합니다.

사용 volatile a[100]{1,2,3,4}GCC는 다음 생성 및 도착 복사 미친 배열을. 일반적으로 volatile컴파일러가 로컬 변수를 초기화하거나 스택에 배치하는 방법을 보는 것이 좋습니다.


1
@Damien 당신은 내 질문을 오해했습니다. 예를 들어, A [0]이 마치 두 값을 할당하는 이유는 요청 a[0] = 0;하고 a[0] = 1;.
Lassie

1
어셈블리를 읽을 수 없지만 배열이 완전히 0으로 채워져 있음을 어디에 표시합니까?
smac89

3
또 다른 흥미로운 사실 ​​: 초기화 된 더 많은 항목의 경우 gcc와 clang은 전체 배열을 복사하는 것으로 되돌아갑니다 .rodata... 400 바이트를 복사하는 것이 0을 설정하고 8 항목을 설정하는 것보다 빠릅니다.
Jester

2
최적화를 비활성화했습니다. 비효율적 인 코드는 동일한 일이 발생하는 것을 확인할 때까지 놀라운 일이 아닙니다 -O3. godbolt.org/z/rh_TNF
Peter Cordes

12
무엇을 더 알고 싶습니까? 그것은 놓친 최적화 missed-optimization입니다. 키워드로 GCC의 버그 질라에보고하십시오 .
Peter Cordes

답변:


2

이론적으로 초기화는 다음과 같습니다.

int a[100] = {
  [3] = 1,
  [5] = 42,
  [88] = 1,
};

따라서 전체 메모리 블록을 먼저 제로화 한 다음 개별 값을 설정하는 것이 캐시 및 최적화 측면에서 더 효과적 일 수 있습니다.

다음에 따라 동작이 변경 될 수 있습니다.

  • 대상 아키텍처
  • 대상 OS
  • 배열 길이
  • 초기화 비율 (명시 적으로 초기화 된 값 / 길이)
  • 초기화 된 값의 위치

물론 배열의 시작 부분에서 초기화가 압축되어 최적화가 쉽지 않습니다.

따라서 gcc가 가장 일반적인 접근 방식 인 것 같습니다. 최적화가 누락 된 것 같습니다.


예, 코드에 대한 최적의 전략은 모든 것을 제로로 만들거나 아마도 a[6]단일 또는 즉각적인 단일 저장소로 채워진 초기 간격으로 시작하여 모든 것을 제로화하는 것 입니다. 특히 x86-64를 대상으로하는 경우 qword 저장소를 사용하여 한 번에 2 개의 요소를 수행 할 수 있습니다 (낮은 요소는 0이 아님). 예 mov QWORD PTR [rsp+3*4], 1를 들어 하나의 잘못 정렬 된 qword 저장소로 요소 3과 4를 수행합니다.
Peter Cordes

이론적으로 행동은 대상 OS에 의존 할 수 있지만 실제 GCC에서는 그렇지 않으며 그럴 이유가 없습니다. 대상 아키텍처 만 (그리고 그 내부에서 -march=skylakevs. -march=k8vs. 와 같은 다른 마이크로 아키텍처에 대한 튜닝 옵션 -march=knl은 일반적으로 매우 다르며, 아마도 이에 대한 적절한 전략 측면에서 다를 수 있습니다.)
Peter Cordes

이것은 C ++에서도 허용됩니까? 나는 그것이 단지 C라고 생각했다.
Lassie

@Lassie 당신은 C ++에서 맞습니다. 이것은 허용되지 않지만 질문은 컴파일러 백엔드와 더 관련이 있으므로 그다지 중요하지 않습니다. 또한 표시된 코드는 둘
다일

일부를 선언 struct Bar{ int i; int a[100]; int j;} 하고 Bar a{1,{2,3,4},4};gcc를 초기화 하여 C ++에서 동일하게 작동하는 예제를 쉽게 구성 할 수도 있습니다 . 모두 0으로 설정 한 다음 5 개의 값을 설정합니다.
vlad_tepesch
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.