C11 표준은 일정한 제어식을 가진 반복문이 최적화되어서는 안된다는 것을 암시하는 것으로 보입니다. 이 답변 에서 조언을 얻었습니다. 표준 초안의 섹션 6.8.5를 구체적으로 인용합니다.
제어 표현식이 상수 표현식이 아닌 반복문은 구현에 의해 가정 될 수 있습니다.
이 답변에서 루프와 같은 루프 while(1) ;
는 최적화되지 않아야한다고 언급합니다 .
그렇다면 … Clang / LLVM은 왜 아래 루프를 최적화 cc -O2 -std=c11 test.c -o test
합니까 (로 컴파일 )?
#include <stdio.h>
static void die() {
while(1)
;
}
int main() {
printf("begin\n");
die();
printf("unreachable\n");
}
내 컴퓨터에서이 인쇄 아웃 begin
후 잘못된 명령에 충돌 의 (a ud2
다음에 위치 트랩 die()
). godbolt 에서는 호출 후 아무것도 생성되지 않음 을 알 수 있습니다 puts
.
Clang이 무한 루프를 출력하도록하는 것은 놀랍게도 어려운 작업이었습니다. 변수를 -O2
반복적으로 테스트 할 수 volatile
는 있지만, 원치 않는 메모리 읽기가 필요합니다. 그리고 내가 이런 식으로하면 :
#include <stdio.h>
static void die() {
while(1)
;
}
int main() {
printf("begin\n");
volatile int x = 1;
if(x)
die();
printf("unreachable\n");
}
… 무한 루프가 존재하지 않는 것처럼 Clang이 인쇄 begin
됩니다 unreachable
.
Clang이 최적화를 켠 상태에서 메모리가없는 적절한 무한 루프를 어떻게 출력하도록합니까?
답변
C11 표준은 6.8.5 / 6이라고 말합니다.
제어 표현식이 상수 표현식이 아닌 반복문입력 / 출력 작업을 수행하지 않고 휘발성 개체에 액세스하지 않으며 본문에서 동기화 또는 원자 적 작업을 수행하지 않는 156) statement) 표현 -3은 구현에 의해 종료 될 수 있다고 가정 할 수있다. 157)
두 발의 메모는 규범이 아니라 유용한 정보를 제공합니다.
156) 생략 된 제어 표현식은 상수 표현식 인 0이 아닌 상수로 대체됩니다.
157) 이것은 종료를 증명할 수없는 경우에도 빈 루프 제거와 같은 컴파일러 변환을 허용하기위한 것입니다.
귀하의 경우, while(1)
맑은 상수 표현이므로 구현에서 종료한다고 가정 하지 않을 수 있습니다. “for-ever”루프가 일반적인 프로그래밍 구조이기 때문에 이러한 구현은 절망적으로 깨질 것입니다.
그러나 루프 후에 “연결할 수없는 코드”에 발생하는 상황은 내가 아는 한 잘 정의되어 있지 않습니다. 그러나 clang은 실제로 매우 이상하게 행동합니다. gcc (x86)와 머신 코드 비교 :
gcc 9.2 -O3 -std=c11 -pedantic-errors
.LC0:
.string "begin"
main:
sub rsp, 8
mov edi, OFFSET FLAT:.LC0
call puts
.L2:
jmp .L2
클랑 9.0.0 -O3 -std=c11 -pedantic-errors
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
gcc는 루프를 생성하고, clang은 단지 숲으로 들어가 오류 255와 함께 종료됩니다.
나는 이것을 준수하지 않는 clang의 행동으로 기울고 있습니다. 나는 당신의 예제를 다음과 같이 확장하려고 시도했기 때문에 :
#include <stdio.h>
#include <setjmp.h>
static _Noreturn void die() {
while(1)
;
}
int main(void) {
jmp_buf buf;
_Bool first = !setjmp(buf);
printf("begin\n");
if(first)
{
die();
longjmp(buf, 1);
}
printf("unreachable\n");
}
_Noreturn
컴파일러를 더 돕기 위해 C11 을 추가했습니다 . 이 키워드만으로도이 기능이 중단된다는 것이 분명해야합니다.
setjmp
는 처음 실행될 때 0을 반환하므로이 프로그램은 while(1)
“sgin”(\ n은 stdout을 플러시한다고 가정) 만 인쇄하고 거기서 중지 해야합니다 . 이것은 gcc에서 발생합니다.
루프가 단순히 제거 된 경우 “시작”을 2 번 인쇄 한 다음 “연결할 수 없음”을 인쇄해야합니다. 그러나 clang ( godbolt )에서는 종료 코드 0을 반환하기 전에 “시작”을 1 회 인쇄 한 다음 “연결할 수 없음”을 인쇄합니다.
여기서 정의되지 않은 동작을 주장하는 경우를 찾을 수 없으므로 필자는 이것이 clang의 버그라는 것입니다. 여하튼,이 동작은 내장 시스템과 같은 프로그램에 clang을 100 % 쓸모 없게 만듭니다. 여기서 감시 프로그램을 기다리는 동안 프로그램을 정지시키는 영원한 루프에 의존 할 수 있어야합니다.
답변
답변
다른 답변은 이미 Clang이 인라인 어셈블리 언어 또는 기타 부작용으로 무한 루프를 방출하는 방법을 다루었습니다. 나는 이것이 실제로 컴파일러 버그인지 확인하고 싶습니다. 특히, 그것은 오래 지속 되는 LLVM 버그 입니다. “부정 효과가없는 모든 루프를 종료해야합니다”라는 C ++ 개념을 C와 같이 사용해서는 안되는 언어에 적용합니다.
예를 들어, Rust 프로그래밍 언어 는 무한 루프를 허용하고 LLVM을 백엔드로 사용하며 동일한 문제가 있습니다.
단기적으로 LLVM은 “부작용이없는 모든 루프를 종료해야한다”고 계속 가정합니다. 무한 루프를 허용하는 모든 언어의 경우 LLVM은 프런트 엔드가 llvm.sideeffect
이러한 루프에 opcode 를 삽입 할 것으로 예상합니다 . 이것이 Rust가 계획하고있는 것이므로 Clang (C 코드를 컴파일 할 때)도 그렇게해야 할 것입니다.
답변
이것은 Clang 버그입니다
… 무한 루프를 포함하는 함수를 인라인 할 때. while(1);
메인에 직접 나타날 때 동작이 다르므로 매우 버그가 있습니다.
참조 Arnavion의 대답 @요약 및 링크는 을 . 이 답변의 나머지 부분은 알려진 버그는 물론 버그인지 확인하기 전에 작성되었습니다.
제목 질문에 대답하려면 : 최적화되지 않은 무한 빈 루프를 어떻게 만들 수 있습니까? ? –
만들 die()
매크로가 아닌 기능을 , 연타 3.9 및 이후 버전에서이 버그를 해결할 수 있습니다. 초기 Clang 버전 은 루프call
를 유지하거나 무한 루프를 사용하여 함수의 비 인라인 버전으로 a 를 내 보냅니다 . print;while(1);print;
함수 가 호출자 ( Godbolt )에 인라인 하더라도 안전 해 보입니다 . -std=gnu11
vs.-std=gnu99
아무것도 변경하지 않습니다.
GNU C에만 관심이 있다면 루프 내부의 P__J____asm__("");
도 작동하며이를 이해하는 컴파일러에 대한 주변 코드의 최적화를 손상시키지 않아야합니다. GNU C Basic asm 문장은 암시 적으로volatile
으로 있으므로 C 추상 시스템 에서처럼 여러 번 “실행”해야하는 눈에 보이는 부작용으로 간주됩니다. (그리고 Clang은 GCC 매뉴얼에 설명 된대로 C의 GNU 방언을 구현합니다.)
일부 사람들은 빈 무한 루프를 최적화하는 것이 합법적 일 수 있다고 주장했습니다. 동의하지는 않지만 1을 동의 하더라도 루프가 도달 할 수없는 후에 Clang이 명령문을 가정 하고 실행이 함수의 끝에서 다음 함수 또는 가비지로 넘어가 는 것은 합법적 이지 않습니다. 무작위 명령으로 해독합니다.
(이것은 Clang ++에 대해 표준을 준수하지만 (아직 유용하지는 않지만) 부작용이없는 무한 루프는 C ++에서는 UB이지만 C
는 아닙니다. while (1); C에서 정의되지 않은 동작은? UB는 컴파일러가 기본적으로 모든 것을 방출하도록합니다. asm
루프에 있는 명령문은 C ++에서이 UB를 피할 것이지만 실제로 C ++로 컴파일하는 경우 인라인 할 때를 제외하고는 상수 표현식 무한 빈 루프를 제거하지 않습니다. C로 컴파일)
while(1);
Clang이 컴파일하는 방식을 수동으로 인라인하여 변경 : 무한 루프가 asm에 존재합니다. 이것이 우리가 변호사 변호사 POV에서 기대하는 것입니다.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
Godbolt 컴파일러 탐색기 에서 -xc
x86-64의 C ( ) 로 컴파일되는 Clang 9.0 -O3 :
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
동일한 옵션을 가진 동일한 컴파일러 는 같은 것을 먼저 main
호출 하는 a 를 컴파일 하지만 다음에 대한 명령어 방출을 중지합니다.infloop() { while(1); }
puts
main
그 시점 이후에 . 내가 말했듯이, 실행은 함수의 끝에서 다음 함수로 넘어갑니다 (그러나 함수 입력에 대해 스택이 잘못 정렬되어 있기 때문에 유효한 tailcall조차 아닙니다).
유효한 옵션은
- 발광
label: jmp label
무한 루프 - 또는 (무한 루프가 제거 될 수 있음을 승인 한 경우) 두 번째 문자열을 인쇄하기 위해 다른 호출을 보낸 다음
return 0
frommain
.
내가 알 수없는 UB가 없으면 C11 구현에 “도달 할 수 없음”을 인쇄하지 않고 충돌하거나 계속 진행하는 것은 분명하지 않습니다.
각주 1 :
레코드에 대해서는 @Lundin의 답변에 동의합니다 .C11 이 비어있는 경우에도 C11이 상수 표현 무한 루프에 대한 종료 가정을 허용하지 않는다는 증거에 대한 표준 을 인용합니다 (I / O, 휘발성, 동기화 또는 기타 없음) 눈에 보이는 부작용).
이것은 일반적인 CPU의 경우 빈 asm 루프로 루프 를 컴파일 할 수있는 조건 세트입니다 . (본문에서 본문이 비어 있지 않은 경우에도 루프가 실행되는 동안 데이터 레이스 UB가 없으면 변수에 대한 할당을 다른 스레드 또는 신호 핸들러에 표시 할 수 없습니다. 따라서 적합한 구현은 원하는 경우 이러한 루프 본문을 제거 할 수 있습니다. 루프 자체를 제거 할 수 있는지에 대한 의문이 남습니다.
C11이 루프 종료를 가정 할 수없고 (UB가 아니라고) 구현할 경우 루프가 런타임에 존재하도록 의도 한 것 같습니다. 무한한 시간에 무한한 양의 작업을 수행 할 수없는 실행 모델로 CPU를 대상으로하는 구현은 빈 상수 무한 루프를 제거 할 정당성이 없습니다. 또는 일반적으로 정확한 표현은 “종료되었다고 가정”할 수 있는지 여부입니다. 루프를 종료 할 수 없으면 수학과 무한대에 대해 어떤 주장을하는지 , 일부 가상 머신에서 무한한 작업을 수행하는 데 걸리는 시간에 관계없이 이후 코드에 도달 할 수 없다는 의미 입니다.
또한 Clang은 단순한 ISO C 호환 DeathStation 9000이 아니라 커널 및 임베디드 기능을 포함한 실제 저수준 시스템 프로그래밍에 유용합니다. 따라서 C11에 대한 제거를 허용 하는 인수를 허용하는지 여부에 관계없이 while(1);
Clang이 실제로 그렇게하고 싶어한다는 것은 의미가 없습니다. 글을 쓰면 while(1);
사고가 아니었을 것입니다. 실수로 무한히 끝나는 루프 (런타임 변수 제어 표현식 사용)를 제거하는 것이 유용 할 수 있으며 컴파일러가 그렇게하는 것이 합리적입니다.
다음 인터럽트까지 방금 돌리고 싶은 경우는 드물지만 C로 쓰면 분명히 예상됩니다. (그리고 무엇 않고 , GCC와 연타에 일어나는 무한 루프는 래퍼 함수 내부에있을 때 연타 제외).
예를 들어, 원시 OS 커널에서 스케줄러에 실행할 태스크가 없으면 유휴 태스크를 실행할 수 있습니다. 그 첫 번째 구현은입니다 while(1);
.
또는 절전 유휴 기능이없는 하드웨어의 경우 이것이 유일한 구현 일 수 있습니다. (2000 년대 초까지는 x86에서는 드물지 않다고 생각했습니다.이 hlt
명령이 존재 하더라도 IDK는 CPU가 저전력 유휴 상태를 시작할 때까지 상당한 양의 전력을 절약했습니다.)
답변
기록을 위해 Clang은 다음과 goto
같이 잘못 작동합니다 .
static void die() {
nasty:
goto nasty;
}
int main() {
int x; printf("begin\n");
die();
printf("unreachable\n");
}
그것은 질문에서와 같은 결과를 산출합니다.
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
나는 C11에서 허용 된대로 이것을 읽을 수있는 방법을 보지 못합니다.
6.8.6.1 (2)
goto
명령문은 둘러싸는 함수에서 이름 지정된 레이블이 붙은 명령문으로 무조건 점프합니다.
대로 goto
에 “반복 문”(6.8.5 나열되지 않습니다 while
, do
및 for
“종료-가정은”면죄부 적용, 그러나 당신이 그들을 읽고 싶은) 특별한에 대해 아무것도.
원래 질문의 Godbolt 링크 컴파일러는 x86-64 Clang 9.0.0이며 플래그는 -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c
x86-64 GCC 9.2와 같은 다른 제품을 사용하면 매우 완벽하게 얻을 수 있습니다.
.LC0:
.string "begin"
main:
sub rsp, 8
mov edi, OFFSET FLAT:.LC0
call puts
.L2:
jmp .L2
플래그 : -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c
답변
나는 악마의 옹호자를 연기하고 표준이 컴파일러가 무한 루프를 최적화하는 것을 명시 적으로 금지하지 않는다고 주장 할 것이다.
입력 / 출력 작업을 수행하지 않고 휘발성 개체에 액세스하지 않으며 본문에서 식을 제어하거나 식을 제어하는 (또는 경우의 경우) 제어식이 상수식이 아닌 반복문 (156) 진술) 표현 -3은 구현에 의해 가정 될 수있다 .157)
이것을 파싱하자. 특정 기준을 만족하는 반복문은 다음과 같이 종료되는 것으로 가정 할 수 있습니다.
if (satisfiesCriteriaForTerminatingEh(a_loop))
if (whatever_reason_or_just_because_you_feel_like_it)
assumeTerminates(a_loop);
이것은 기준이 충족되지 않고 루프가 종료 될 수 있다고 가정하더라도 표준의 다른 규칙이 준수되는 한 명시 적으로 금지되지 않는다고 가정하는 경우에 대해서는 아무 것도 말하지 않습니다.
do { } while(0)
또는 while(0){}
모든 반복문 (루프) 후에 컴파일러가 종료한다고 가정 할 수있는 기준을 만족시키지 못하지만 분명히 종료합니다.
그러나 컴파일러가 최적화 while(1){}
할 수 있습니까?
5.1.2.3p4의 말 :
추상 머신에서 모든 표현식은 시맨틱에 의해 지정된대로 평가됩니다. 실제 구현에서는 값을 사용하지 않고 필요한 부작용이 발생하지 않는다고 추정 할 수있는 경우 (함수 호출 또는 휘발성 개체 액세스로 인한 영향 포함) 표현식의 일부를 평가할 필요가 없습니다.
이것은 진술이 아닌 표현을 언급하므로 100 % 확신 할 수는 없지만 확실히 다음과 같은 호출을 허용합니다.
void loop(void){ loop(); }
int main()
{
loop();
}
건너 뛸 수 있습니다. 흥미롭게도 clang은 그것을 건너 뛰고 gcc는 그렇지 않습니다 .
답변
나는 이것이 단순한 오래된 버그라고 확신했다. 나는 나의 시험을 아래에 남겨두고, 특히 내가 이전에 가지고 있었던 몇 가지 이유에 대해 표준위원회의 토론에 대한 언급을 남긴다.
나는 이것이 정의되지 않은 행동이라고 생각하고 (끝 참조) Clang은 단지 하나의 구현을 가지고 있습니다. GCC는 실제로 예상대로 작동하여 unreachable
인쇄 문만 최적화 하고 루프를 남겨 둡니다. Clang이 인라인을 결합하고 루프로 수행 할 수있는 작업을 결정할 때 이상한 결정을 내리는 방법.
동작은 매우 이상합니다. 최종 인쇄를 제거하므로 무한 루프를 “보고”루프를 제거합니다.
내가 알 수있는 한 훨씬 더 나쁩니다. 우리가 얻는 인라인 제거 :
die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
jmp .LBB0_1
main: # @main
push rax
mov edi, offset .Lstr
call puts
.Lstr:
.asciz "begin"
따라서 함수가 생성되고 호출이 최적화됩니다. 이것은 예상보다 훨씬 탄력적입니다.
#include <stdio.h>
void die(int x) {
while(x);
}
int main() {
printf("begin\n");
die(1);
printf("unreachable\n");
}
함수에 대해 최적화되지 않은 어셈블리가 발생하지만 함수 호출이 다시 최적화됩니다! 더 나쁜 :
void die(x) {
while(x++);
}
int main() {
printf("begin\n");
die(1);
printf("unreachable\n");
}
나는 로컬 변수를 추가하고 그것을 늘리고 포인터를 전달하고 goto
등을 사용하여 다른 테스트를 많이했습니다 .이 시점에서 나는 포기할 것입니다. clang을 사용해야하는 경우
static void die() {
int volatile x = 1;
while(x);
}
일을한다. 최적화 (분명히)에 빠지고 중복 된 final에 남습니다 printf
. 최소한 프로그램은 멈추지 않습니다. 어쩌면 GCC?
추가
David와의 논의에 따라 표준에 “조건이 일정하면 루프가 종료된다고 가정하지 않을 것”이라고 말하지 않습니다. 따라서 표준에서 허용되고 관찰 할 수있는 행동이 없다 (표준에 정의 된 바와 같이), 나는 일관성에 대해서만 논쟁 할 것입니다. 컴파일러가 루프를 종료한다고 가정하여 루프를 최적화하는 경우 다음 명령문을 최적화하지 않아야합니다.
Heck n1528 은 이것을 올바르게 읽으면 정의되지 않은 동작으로 나타납니다. 구체적으로 특별히
그렇게하는 주요 문제는 코드가 잠재적으로 종료되지 않는 루프를 가로 질러 이동할 수 있다는 것입니다
여기에서 나는 그것이 허용되는 것보다는 우리가 원하는 것 (예상되는 것)에 대한 토론으로 만 나눌 수 있다고 생각합니다 .