gcc는 왜 구조체로부터 추측적으로 적재할 수 있는가?
결함이 발생할 수 있는 gcc 최적화 및 사용자 코드 표시 예제
아래 코드 조각의 'foo' 함수는 구조체 멤버 A 또는 B 중 하나만 로드된다. 적어도 그것은 최적화되지 않은 코드의 의도다.
typedef struct {
int A;
int B;
} Pair;
int foo(const Pair *P, int c) {
int x;
if (c)
x = P->A;
else
x = P->B;
return c/102 + x;
}
gcc -O3의 내용은 다음과 같다.
mov eax, esi
mov edx, -1600085855
test esi, esi
mov ecx, DWORD PTR [rdi+4] <-- ***load P->B**
cmovne ecx, DWORD PTR [rdi] <-- ***load P->A***
imul edx
lea eax, [rdx+rsi]
sar esi, 31
sar eax, 6
sub eax, esi
add eax, ecx
ret
그래서 gcc는 분점을 없애기 위해 두 구조부재를 모두 투기적으로 적재할 수 있는 것으로 보인다.그러나 그렇다면 다음 코드는 정의되지 않은 동작으로 간주되는 것인가, 아니면 위의 gcc 최적화가 불법적인 것인가?
#include <stdlib.h>
int naughty_caller(int c) {
Pair *P = (Pair*)malloc(sizeof(Pair)-1); // *** Allocation is enough for A but not for B ***
if (!P) return -1;
P->A = 0x42; // *** Initializing allocation only where it is guaranteed to be allocated ***
int res = foo(P, 1); // *** Passing c=1 to foo should ensure only P->A is accessed? ***
free(P);
return res;
}
위의 시나리오에서 하중 투기가 발생할 경우, P->B의 마지막 바이트가 할당되지 않은 메모리에 있을 수 있기 때문에 P->B 로딩이 예외를 야기할 가능성이 있다.최적화가 꺼진 경우 이 예외는 발생하지 않는다.
질문
상기의 하중 투기에 대한 gcc 최적화가 합법적인가?스펙은 어디에 괜찮다고 하는가, 암시를 주는가?최적화가 합법적인 경우, 'naughtly_caller'의 코드가 어떻게 정의되지 않은 동작으로 판명되는가?
변수 읽기(다른 이름으로 선언되지 않음)volatile
)는 C 표준에서 명시한 "부작용"으로 간주되지 않는다.그래서 프로그램은 C 표준에 관한 한 자유롭게 위치를 읽고 그 결과를 폐기할 수 있다.
이것은 매우 흔하다.4바이트 정수로 1바이트의 데이터를 요청한다고 가정합시다.그러면 컴파일러는 32비트가 더 빠를 경우(정렬 읽기) 전체를 읽은 다음 요청된 바이트를 제외한 모든 것을 폐기할 수 있다.너의 예는 이것과 비슷하지만 컴파일러는 구조 전체를 읽기로 결정했다.
공식적으로 이것은 "추상적 기계" C11장 5.1.2.3의 동작에서 발견된다.컴파일러가 거기에 명시된 규칙을 따른다는 점을 감안하면 마음대로 해도 무방하다.그리고 열거된 유일한 규칙은volatile
지시사항의 목적 및 순서 지정.에서 다른 구조체 멤버 읽기volatile
구조는 좋지 않을 것이다.
구조체 전체에 너무 적은 메모리를 할당하는 경우, 그것은 정의되지 않은 행동이다.왜냐하면 구조물의 메모리 레이아웃은 대개 프로그래머가 결정할 수 있는 것이 아니기 때문이다. 예를 들어 컴파일러는 마지막에 패딩을 추가할 수 있다.할당된 메모리가 충분하지 않으면 코드가 구조체의 첫 번째 멤버에서만 작동하더라도 금지된 메모리에 액세스하게 될 수 있다.
아니, 만약*P
올바르게 할당됨P->B
절대 할당되지 않은 기억 속에 있을 수 없어그것은 초기화가 되지 않을 수도 있다, 그것이 전부다.
컴파일러는 그들이 하는 일을 할 모든 권리가 있다.유일하게 허락되지 않는 것은 출입에 대해 opp하는 것이다.P->B
초기억지하다 는 핑로로. 이 이 것을 그러나 그들이 이 모든 것을 어떻게 어떻게 하는지는 실행의 재량에 달려 있는 것이지, 당신의 관심사가 아니다.
다음에 의해 반환되는 블록에 포인터를 캐스팅할 경우malloc
로Pair*
a를 담을 수 있을 정도로 넓다고 보장되지 않는Pair
네 프로그램의 동작은 정의되지 않았다.
A ->
A의 교환원.Pair *
전부가 있다는 것을 암시한다.Pair
완전히 할당된 물체. (@Hurkyl은 표준을 인용한다.)
x86(일반 아키텍처와 마찬가지로)은 일반 할당된 메모리에 접근하는 데 부작용이 없으므로, x86 메모리 의미론은 비메모리용volatile
C 추상기계의 의미론과 호환된다.컴파일러는 특정 상황에서 조정 중인 마이크로아키텍처 대상의 성능 승리가 될 것으로 생각될 경우 투기적으로 로딩할 수 있다.
x86에서는 메모리 보호가 페이지 세분화와 함께 작동한다는 점에 유의하십시오.컴파일러는 접촉한 모든 페이지가 개체의 바이트를 포함하고 있는 한 개체 밖에서 읽는 방식으로 루프를 풀거나 SIMD로 벡터화할 수 있다.x86과 x64의 동일한 페이지 내에서 버퍼 끝을 지나서 읽어도 안전한가?libcstrlen()
조립 시 손으로 작성한 구현에서는 이렇게 하지만 AFAIK gcc는 그렇지 않고, 대신 포인터를 (완전히 롤링되지 않은) 시작 루프와 정렬시킨 곳에서도 자동 벡터링된 루프 끝에 남은 요소들에 스칼라 루프를 사용한다. (아마도, 이 루프는 런타임 한계 확인을 하기 때문이다.)valgrind
난해한
예상한 행동을 얻으려면, 아그를 사용하십시오.
어레이는 단일 객체지만 포인터들은 어레이와는 다르다.(두 어레이 요소가 모두 액세스할 수 있는 것으로 알려진 컨텍스트에 기댄다고 해도, 구조체 코드가 승리라면, 어레이에서도 역시 안전할 때 하지 않는 것이 gcc의 코드 발출을 가져올 수 없었다.
C에서는 이 함수를 단일 함수에 포인터로 전달할 수 있다.int
하는 한은c
0이 아니다.x86용으로 컴파일할 때 gcc는 마지막을 가리킬 수 있다고 가정해야 한다.int
다음 페이지가 매핑되지 않은 상태에서.
Godbolt 컴파일러 탐색기의 이것과 다른 변형에 대한 소스 + gcc 및 cang 출력
// exactly equivalent to const int p[2]
int load_pointer(const int *p, int c) {
int x;
if (c)
x = p[0];
else
x = p[1]; // gcc missed optimization: still does an add with c known to be zero
return c + x;
}
load_pointer: # gcc7.2 -O3
test esi, esi
jne .L9
mov eax, DWORD PTR [rdi+4]
add eax, esi # missed optimization: esi=0 here so this is a no-op
ret
.L9:
mov eax, DWORD PTR [rdi]
add eax, esi
ret
C에서는 어레이 객체를 함수에 전달(참조)할 수 있으며, C 추상화 기계가 그렇지 않더라도 모든 메모리를 만질 수 있는 기능을 보장한다.구문은
int load_array(const int p[static 2], int c) {
... // same body
}
그러나 gcc는 활용하지 않고 load_pointer에 동일한 코드를 방출한다.
Off 항목: clang은 a를 사용하여 모든 버전(구조 및 배열)을 동일한 방식으로 컴파일함cmov
부하 주소를 분간하여 계산한다.
lea rax, [rdi + 4]
test esi, esi
cmovne rax, rdi
add esi, dword ptr [rax]
mov eax, esi # missed optimization: mov on the critical path
ret
이것은 반드시 좋은 것만은 아니다. 부하 주소는 몇 개의 추가 ALU ops에 의존하기 때문에 gcc의 구조 코드보다 대기 시간이 더 길다.두 주소 모두 읽기에 안전하지 않고 지점의 예측이 서툴다면 꽤 좋다.
우리는 gcc와 clang으로부터 동일한 전략에 대해 더 나은 코드를 얻을 수 있다.setcc
(실제로 오래된 CPU를 제외한 모든 CPU에서 1c 지연 시간을 갖는 1 up) 대신cmovcc
(Skylake 이전 인텔에서 ups 2개). xor
로잉은 LEA보다 싸다.
int load_pointer_v3(const int *p, int c) {
int offset = (c==0);
int x = p[offset];
return c + x;
}
xor eax, eax
test esi, esi
sete al
add esi, dword ptr [rdi + 4*rax]
mov eax, esi
ret
gcc와 땡땡이 둘 다 결승전을 치렀다.mov
인텔 패밀리의 가 인텔 샌디브리지 패밀리와 마이크로퓨즈로 유지되지 않는다add
분기 버전에서 하는 것과 같이 이것이 더 나을 것이다.
xor eax, eax
test esi, esi
sete al
mov eax, dword ptr [rdi + 4*rax]
add eax, esi
ret
다음과 같은 간단한 주소 지정 모드[rdi]
또는[rdi+4]
SnB SnB 제품군 CPU에 의한 대기 시간이 1c로 스카이레이크(Skylake)의 대기 더 수 있다.cmov
값이 싸다.그test
그리고lea
병렬로 달릴 수 있다.
인라이닝 후, 그 결승전은mov
아마도 존재하지 않을 것이고, 그것은 단지add
esi
.
일반적인 경우에서 어떤 기억 위치를 읽는 것은 관찰할 수 있는 행동으로 간주되지 않기 때문에 이것은 완벽하게 합법적이다.volatile
이를 변화시킬 것이다.
당신의 예시 코드는 정말 정의되지 않은 행동이지만, 나는 표준 문서에서 이것을 명시적으로 언급하는 어떤 구절도 찾을 수 없다.그러나 나는 N1570, §6.5 p6의 유효 유형에 대한 규칙을 살펴보는 것으로 충분하다고 생각한다.
문자 유형이 아닌 형식을 가진 lvalue를 통해 선언된 유형이 없는 객체에 값을 저장하면, lvalue 유형은 해당 액세스와 저장된 값을 수정하지 않는 후속 액세스에 대한 객체의 유효 유형이 된다.
에 대한 쓰기 액세스 권한*P
실제로 저 물체에 그런 유형을 부여한다.Pair
-- 따라서 할당하지 않은 메모리로만 확장되며, 그 결과는 범위를 벗어난 액세스였습니다.
사후 수정이 뒤따르는 것은
->
운영자와 식별자는 구조물 또는 조합 객체의 구성원을 지정한다.이다.
식을 호출하는 경우P->A
그렇다면, 잘 정립되어 있다.P
실제로 어떤 종류의 물체를 가리켜야 한다.struct Pair
, 그리고 결과적으로.P->B
또한 잘 정의되어 있다.
이는 적합한 프로그램이 차이를 구별할 수 없는 경우 "있는 그대로" 규칙에 따라 항상 허용된다.예를 들어, 구현은 각 블록이 malloc로 할당된 후 부작용 없이 접근 가능한 최소 8바이트가 있음을 보장할 수 있다.그 상황에서 컴파일러는 당신이 당신의 코드에 그것을 썼다면 정의되지 않은 행동일 코드를 생성할 수 있다.따라서 컴파일러는 P[0]가 올바르게 할당될 때마다 P[1]를 읽는 것이 합법적일 수 있다. 비록 그것이 자신의 코드에 정의되지 않은 행동일지라도 말이다.
그러나 당신의 경우, 만약 당신이 구조체에 충분한 메모리를 할당하지 않는다면, 어떤 멤버라도 읽는 것은 정의되지 않은 행동이다.따라서 여기서 컴파일러는 P->B 읽기가 충돌하더라도 이렇게 할 수 있다.
참조URL: https://stackoverflow.com/questions/46522451/why-is-gcc-allowed-to-speculatively-load-from-a-struct
'Programing' 카테고리의 다른 글
경로 매개 변수에 배치할 계산된 속성 항목에 개체 항목을 추가하는 방법 (0) | 2022.04.18 |
---|---|
C에서 문자열을 올바르게 비교하려면 어떻게 해야 하는가? (0) | 2022.04.18 |
SSR을 지원하는 Vue-cli 3 구성 요소 라이브러리 (0) | 2022.04.18 |
vuex의 API에서 데이터를 올바르게 가져오는 방법 (0) | 2022.04.18 |
기능 포인터를 포맷하는 방법? (0) | 2022.04.17 |