최적화되지 않는 무한 빈 루프를 만들려면 어떻게해야합니까?


131

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이 최적화를 켠 상태에서 메모리가없는 적절한 무한 루프를 어떻게 출력하도록합니까?


3
의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Bhargav Rao

2
부작용이없는 휴대용 솔루션은 없습니다. 메모리 액세스를 원하지 않으면 휘발성 부호없는 문자를 등록하는 것이 가장 좋습니다. 그러나 레지스터는 C ++ 17에서 사라집니다.
스콧 M

25
어쩌면 이것이 질문의 범위에 속하지는 않지만, 왜 당신이 이것을하고 싶어하는지 궁금합니다. 실제 작업을 수행하는 다른 방법이 있습니다. 아니면 이것은 단지 학문적입니까?
Cruncher

1
@Cruncher : 프로그램을 실행하려는 특정 시도의 효과는 유용 할 수 있습니다. 프로그램이 무한 루프에 빠지는 실행은 쓸모가 없지만 컴파일러가 대체 할 수있는 다른 동작보다 선호됩니다.
supercat

6
@Cruncher :의 개념이없는 독립형 컨텍스트에서 코드가 실행 중일 수 있고 코드가 exit()지속적인 실행의 효과 가 쓸모없는 것보다 나쁘지 않다고 보장 할 수없는 상황을 발견했을 수 있기 때문 입니다. 자체적으로 점프하는 루프는 그러한 상황을 처리하기에는 아주 형편없는 방법이지만 나쁜 상황을 처리하는 가장 좋은 방법 일 수 있습니다.
supercat

답변:


77

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 % 쓸모 없게 만듭니다. 여기서 감시 프로그램을 기다리는 동안 프로그램을 정지시키는 영원한 루프에 의존 할 수 있어야합니다.


15
나는 "이것은 명백한 상수 표현이기 때문에 구현이 종료한다고 가정하지 않을 수있다" 고 동의하지 않는다 . 이것은 엄밀한 언어 변호사에 실제로 도달하지만 if (이들)6.8.5/6 의 형태 로 당신은 (this)라고 가정 할 수 있습니다 . 그렇다고 귀하가 (이) 가정 하지 않을 수도 있다는 의미는 아닙니다 . 표준을 사용하여 원하는 작업을 수행 할 수있는 곳이 아닌 조건이 충족 될 때만 사양입니다. 그리고 관찰 가능한 것이 없다면
kabanus

7
@kabanus 인용 된 부분은 특별한 경우입니다. 그렇지 않은 경우 (특별한 경우) 평소와 같이 코드를 평가하고 순서를 지정하십시오. 동일한 장을 계속 읽으면 인용 된 특수 경우를 제외하고 제어 표현식이 각 반복문에 대해 지정된대로 평가됩니다 ( "시맨틱으로 지정된대로"). 그것은 순서가 잘 정의 된 모든 값 계산의 평가와 동일한 규칙을 따릅니다.
룬딘

2
나는 동의하지만, 당신은 어셈블리에 int z=3; int y=2; int x=1; printf("%d %d\n", x, z);없다고 생각하지 않을 것 2입니다. 따라서 빈 쓸모없는 의미 에서 최적화 이후에 x할당되지 않은 후에 할당되었습니다 . 따라서 마지막 문장에서 우리는 규칙을 따르고, (우리가 더 잘 구속되지 않았기 때문에) 정지 된 것으로 가정하고 최종 "도달 할 수없는"인쇄물에 남았습니다. 이제 우리는 그 쓸모없는 진술을 최적화합니다 (우리는 더 잘 알지 못하기 때문에). yz
kabanus

2
@MSalters 내 의견 중 하나가 삭제되었지만 입력 해 주셔서 감사합니다. 동의합니다. 내 의견이 말한 것은 이것이 토론의 핵심이라고 생각합니다 . 논리가 소스에 남아 있더라도 어떤 의미론을 최적화 할 수 있는지에 while(1);대한 int y = 2;진술 과 동일 합니다. n1528부터 나는 그들이 똑같을 것이라는 인상을 받았다. 그러나 사람들이 나보다 더 많은 경험을 가지고 있기 때문에 다른 방식으로 논쟁하고 있으며, 공식적인 버그이기 때문에 표준의 문구가 명시 적인지에 대한 철학적 논쟁을 넘어서 인수는 무질서하게 렌더링됩니다.
kabanus

2
" 'for-ever'루프가 일반적인 프로그래밍 구성이므로 이러한 구현은 절망적으로 깨질 것입니다." — 나는 감정을 이해하지만 인수가 C ++에 동일하게 적용될 수 있기 때문에 그 결점에 결함이 있지만,이 루프를 최적화 한 C ++ 컴파일러는 깨지지 않고 적합합니다.
콘래드 루돌프

52

부작용을 일으킬 수있는 표현식을 삽입해야합니다.

가장 간단한 해결책 :

static void die() {
    while(1)
       __asm("");
}

Godbolt 링크


21
그러나 clang이 왜 작동하는지 설명하지 않습니다.
룬딘

4
"클랑의 버그"라고 말하는 것만으로 충분합니다. "버그"를 외치기 전에 먼저 여기 몇 가지를 시도하고 싶습니다.
룬딘

3
@Lundin 나는 그것이 버그인지 모른다. 이 경우 표준은 기술적으로 정확하지 않습니다
P__J__

4
다행히 GCC는 오픈 소스이며 예제를 최적화하는 컴파일러를 작성할 수 있습니다. 그리고 지금 그리고 미래에 당신이 생각 해낸 어떤 예에서도 그렇게 할 수 있습니다.
토마스 웰러

3
@ThomasWeller : GCC 개발자는이 루프를 최적화하는 패치를 허용하지 않습니다. 그것은 문서화 된 = 보증 된 행동을 위반할 것입니다. 내 이전 의견을 참조하십시오 : asm("")is 묵시적 asm volatile("");이므로 asm 문은 추상 시스템 gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html 에서와 같이 여러 번 실행해야합니다 . (주이 있다고 하지 부작용은 어떤 메모리 나 레지스터를 포함하는 안전, 당신은으로 확장 ASM 필요 "memory"는 C. 기본 ASM에서 혹시 액세스 같은 것들에 대해서만 안전하다는 것을 읽거나 쓰기 메모리하려는 경우 소지품 asm("mfence")또는 cli.)
Peter Cordes

50

다른 답변은 이미 Clang이 인라인 어셈블리 언어 또는 기타 부작용으로 무한 루프를 방출하는 방법을 다루었습니다. 나는 이것이 실제로 컴파일러 버그인지 확인하고 싶습니다. 특히, 그것은 오래 지속 되는 LLVM 버그 입니다. "부정 효과가없는 모든 루프를 종료해야합니다"라는 C ++ 개념을 C와 같이 사용해서는 안되는 언어에 적용합니다.

예를 들어, Rust 프로그래밍 언어 는 무한 루프를 허용하고 LLVM을 백엔드로 사용하며 동일한 문제가 있습니다.

단기적으로 LLVM은 "부작용이없는 모든 루프를 종료해야한다"고 계속 가정합니다. 무한 루프를 허용하는 모든 언어의 경우 LLVM은 프런트 엔드가 llvm.sideeffect이러한 루프에 opcode 를 삽입 할 것으로 예상합니다 . 이것이 Rust가 계획하고있는 것이므로 Clang (C 코드를 컴파일 할 때)도 그렇게해야 할 것입니다.


5
제안 된 여러 수정 및 패치가 있지만 아직 수정되지 않은 10 년 이상 된 버그의 냄새와 같은 것은 없습니다.
이안 켐

4
@IanKemp : 버그를 수정하려면 버그를 수정하는 데 10 년이 걸렸다는 사실을 인정해야합니다. 표준이 그들의 행동을 정당화하기 위해 바뀔 것이라는 희망을 갖는 것이 좋습니다. 물론, 표준이 변경 되더라도 표준의 변경이 표준의 초기 행동 명령이 소급 적으로 수정되어야하는 결함이라는 표시로 간주하려는 사람들의 눈을 제외하고는 여전히 그들의 행동을 정당화하지는 않을 것입니다.
supercat

4
LLVM이 sideeffectop를 추가했다는 의미에서 "고정"되었습니다 (2017 년). 프런트 엔드가 재량에 따라 해당 op를 루프에 삽입 할 것으로 예상합니다. LLVM은 골라야 일부 루프의 기본을, 그리고 의도적으로하거나, C ++의 행동에 맞춰 하나를 선택하기 위해 일어났다. 물론 연속 sideeffectop를 하나로 병합하는 것과 같이 아직 최적화 작업이 남아 있습니다 . (이것은 Rust 프론트 엔드가 그것을 사용하지 못하게 막는 것입니다.) 따라서 버그는 프론트 엔드 (clang)에 있으며 루프에 op를 삽입하지 않습니다.
Arnavion

@Arnavion : 결과가 사용되지 않는 한 또는 결과가 사용되기 전까지 작업이 지연 될 수 있음을 나타내는 방법이 있습니까? 그러나 데이터로 인해 프로그램이 끝없이 반복되는 경우 과거의 데이터 종속성을 진행하면 프로그램 이 쓸모없는 것보다 나빠질 수 있습니다 . 최적화 프로그램이 쓸모없는 것보다 프로그램을 더 나쁘게 만드는 것을 막기 위해 이전의 유용한 최적화를 방해하는 가짜 부작용을 추가해야한다는 것은 효율성을위한 레시피처럼 들리지 않습니다.
슈퍼 캣

이 토론은 아마도 LLVM / clang 메일 링리스트에 속할 것입니다. 그러나 op를 추가 한 LLVM 커밋은 몇 가지 최적화 과정을 가르쳐주었습니다. 또한 Rust sideeffect는 모든 함수의 시작 부분에 op를 삽입 하여 실험했으며 런타임 성능 회귀를 보지 못했습니다. 유일한 문제는 컴파일 타임 회귀입니다. 이전 의견에서 언급 한 것처럼 연속 op의 융합이 없기 때문입니다.
Arnavion

32

이것은 Clang 버그입니다

... 무한 루프를 포함하는 함수를 인라인 할 때. while(1);메인에 직접 나타날 때 동작이 다르므로 매우 버그가 있습니다.

참조 Arnavion의 대답 @요약 및 링크는 을 . 이 답변의 나머지 부분은 알려진 버그는 물론 버그인지 확인하기 전에 작성되었습니다.


제목 질문에 대답하려면 : 최적화되지 않은 무한 빈 루프를 어떻게 만들 수 있습니까? ? -
만들 die()매크로가 아닌 기능을 , 연타 3.9 및 이후 버전에서이 버그를 해결할 수 있습니다. 초기 Clang 버전 은 루프call유지하거나 무한 루프를 사용하여 함수의 비 인라인 버전으로 a 를 내 보냅니다 . print;while(1);print;함수 호출자 ( Godbolt )에 인라인 하더라도 안전 해 보입니다 . -std=gnu11vs.-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 컴파일러 탐색기 에서 -xcx86-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); }putsmain 그 시점 이후에 . 내가 말했듯이, 실행은 함수의 끝에서 다음 함수로 넘어갑니다 (그러나 함수 입력에 대해 스택이 잘못 정렬되어 있기 때문에 유효한 tailcall조차 아닙니다).

유효한 옵션은

  • 발광 label: jmp label무한 루프
  • 또는 (무한 루프가 제거 될 수 있음을 승인 한 경우) 두 번째 문자열을 인쇄하기 위해 다른 호출을 보낸 다음 return 0from main.

내가 알 수없는 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가 저전력 유휴 상태를 시작할 때까지 상당한 양의 전력을 절약했습니다.)


1
궁금해하는 사람은 실제로 임베디드 시스템에 clang을 사용하고 있습니까? 나는 그것을 본 적이 없으며 전적으로 임베디드로 작업합니다. gcc는 단지 "최근"(10 년 전) 임베디드 시장에 진입했으며,이를 바람직하게는 최적화 수준이 낮고 항상로 사용 -ffreestanding -fno-strict-aliasing합니다. ARM 및 레거시 AVR에서 잘 작동합니다.
룬딘

1
@Lundin : 임베디드에 관한 IDK. 그러나 사람들은 적어도 때때로 리눅스 인 clang으로 커널을 빌드합니다. 아마도 MacOS 용 Darwin 일 것입니다.
Peter Cordes

2
bugs.llvm.org/show_bug.cgi?id=965 이 버그는 관련이있는 것으로 보이지만 현재보고있는 것이 확실하지 않습니다.
bracco23

1
@ lundin-우리는 VxWorks 및 PSOS와 같은 RTOS와 함께 90 년대 내내 임베디드 작업에 GCC (및 다른 많은 툴킷)를 사용했다고 확신합니다. GCC가 최근 임베디드 시장에 진입 한 이유를 모르겠습니다.
Jeff Learman

1
@JeffLearman 그러면 최근에 주류가 되었습니까? 어쨌든, gcc 엄격한 앨리어싱 FIASCO는 C99를 도입 한 후에 만 ​​발생했으며, 더 최신 버전의 앨리어싱은 더 이상 앨리어싱 위반이 발생해도 더 이상 바나나가되지 않습니다. 그래도 사용할 때마다 회의론을 유지합니다. clang의 경우 최신 버전은 영구 루프와 관련하여 완전히 손상되었으므로 임베디드 시스템에는 사용할 수 없습니다.
Lundin

14

기록을 위해 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, dofor"종료-가정은"면죄부 적용, 그러나 당신이 그들을 읽고 싶은) 특별한에 대해 아무것도.

원래 질문의 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


적합한 구현은 실행 시간 또는 CPU주기에 대해 문서화되지 않은 변환 제한을 가질 수 있으며,이 제한을 초과하거나 프로그램 입력이 제한을 초과하는 경우 임의의 동작이 발생할 수 있습니다. 그러한 것은 표준의 관할권 밖에서 실행 품질 문제입니다. clang의 관리자가 품질이 좋지 않은 구현을 할 권리에 대해 일관성이 없을 것 같지만 표준은 허용합니다.
supercat

2
@supercat 댓글 주셔서 감사합니다 ... 왜 번역 제한을 초과하면 번역 단계를 실패하고 실행을 거부하는 것 이외의 다른 작업을 수행합니까? 또한 : " 5.1.1.3 진단 적합한 구현은 ... 전처리 번역 단위 또는 번역 단위에 구문 규칙 또는 제약 조건 의 위반이 포함 된 경우 ... 진단 메시지를 생성해야 합니다 ...". 실행 단계에서 잘못된 동작이 어떻게 적용되는지 알 수 없습니다.
조나단

구현 시간에 구현 제한이 모두 해결되어야한다면 표준은 구현하기가 불가능할 것입니다. 왜냐하면 우주에 원자보다 많은 바이트가 필요한 엄격한 준수 프로그램을 작성할 수 있기 때문입니다. 런타임 제한이 "번역 제한"으로 집중되어야하는지 확실하지 않지만, 그러한 양보가 분명히 필요하며 다른 범주는 없습니다.
supercat

1
"번역 한도"에 대한 귀하의 의견에 응답했습니다. 물론 실행 제한도 있습니다. 번역 제한으로 묶어야한다고 제안하는 이유 또는 왜 필요한지 이해하지 못합니다. 나는 nasty: goto nasty사용자 또는 리소스 고갈이 개입 할 때까지 CPU가 회전하고 말하지 않는 이유를 알지 못합니다.
jonathanjo

1
표준은 내가 찾을 수있는 "실행 제한"에 대한 언급을하지 않습니다. 함수 호출 중첩과 같은 것은 일반적으로 스택 할당에 의해 처리되지만 함수 호출을 깊이 16으로 제한하는 적합한 구현은 모든 함수의 16 개 사본을 빌드 할 수 있으며 bar()내부 foo()에서 __1foo에 대한 호출은에서 로 __2bar,에서 __2foo__3bar, 등을에서 __16foo__launch_nasal_demons모든 자동 객체가 정적으로 할당하고, 무엇을 만들 것입니다 수있는 것이다, 일반적으로 번역 한계로 "런타임"제한.
supercat

5

나는 악마의 옹호자를 연기하고 표준이 컴파일러가 무한 루프를 최적화하는 것을 명시 적으로 금지하지 않는다고 주장 할 것이다.

입력 / 출력 작업을 수행하지 않고 휘발성 개체에 액세스하지 않으며 본문에서 식을 제어하거나 식을 제어하는 ​​(또는 경우의 경우) 제어식이 상수식이 아닌 반복문 (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는 그렇지 않습니다 .


"이것은 기준이 충족되지 않으면 어떤 일이 발생하는지에 대해서는 아무 말도하지 않습니다."그러나 6.8.5.1 while 문 : "제어식의 평가는 루프 본문이 실행될 때마다 실행됩니다." 그게 다야. 이것은 (상수 표현의) 값 계산이며, 평가라는 용어를 정의하는 추상 기계 5.1.2.3의 규칙에 속한다 : " 일반적으로 표현의 평가 는 값 계산과 부작용의 시작을 포함한다." 그리고 같은 장에 따르면, 그러한 모든 평가는 시맨틱에 의해 지정된대로 순서화되고 평가된다.
룬딘

1
@Lundin 그래서 while(1){}무한한 1평가 순서가 평가와 얽혀 {}있지만, 그 평가에서 0이 아닌 시간 필요하다는 표준은 어디에 있습니까? gcc 동작이 더 유용하다고 생각합니다. cuz는 메모리 액세스와 관련된 트릭이나 언어 외부의 트릭이 필요하지 않습니다. 그러나 나는 표준이 clang 에서이 최적화를 금지한다고 확신하지 않습니다. while(1){}최적화 할 수 없게 만드는 것이 의도라면 표준에 대해 명시해야하며 무한 반복은 5.1.2.3p2에서 관찰 가능한 부작용으로 나열되어야합니다.
PSkocik

1
1조건을 값 계산으로 취급하면 지정되어 있다고 생각합니다 . 실행 시간은 중요하지 않습니다 - 어떤 중요한 것은 무엇을 while(A){} B;할 수 없습니다 에 최적화되지 멀리 완전히 최적화 B;와에-염기 서열을 다시하지 B; while(A){}. C11 추상 머신 강조 광산 인용 "식 A 및 B의 평가 사이의 시퀀스 지점이 존재하는 것을 의미 모든 값 계산 및 부작용 A의 관련된 모든 값을 계산하기 전에 서열화 및 부작용 B와 연관된 ." 의 값은 A루프에서 명확하게 사용됩니다.
룬딘

2
+1 비록 "실행없이 아무 것도없이 실행이 멈추는 것"이 ​​진공의 표준을 넘어서서 의미가 있고 유용한 "부작용"의 정의에서 "부작용"인 것처럼 보이지만 이것은 설명에 도움이됩니다. 누군가에게 이해할 수있는 사고 방식.
mtraceur

1
"무한 루프 최적화" 근처 : "it" 이 표준인지 컴파일러 인지를 명확하게 알 수는 없습니다 . 을 감안할 때 "아마해야하지만" 이 아니라 "아마 안하지만" 그것이 그 표준 아마도 "은" 을 의미는.
피터 Mortensen

2

나는 이것이 단순한 오래된 버그라고 확신했다. 나는 나의 시험을 아래에 남겨두고, 특히 내가 이전에 가지고 있었던 몇 가지 이유에 대해 표준위원회의 토론에 대한 언급을 남긴다.


나는 이것이 정의되지 않은 행동이라고 생각하고 (끝 참조) 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 은 이것을 올바르게 읽으면 정의되지 않은 동작으로 나타납니다. 구체적으로 특별히

그렇게하는 주요 문제는 코드가 잠재적으로 종료되지 않는 루프를 가로 질러 이동할 수 있다는 것입니다

여기에서 나는 그것이 허용되는 것보다는 우리가 원하는 것 (예상되는 것)에 대한 토론으로 만 나눌 수 있다고 생각합니다 .


의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Bhargav Rao

다시 "일반 모든 버그" : 당신은 의미합니까 " 평범한 오래된 버그" ?
피터 Mortensen

@PeterMortensen "ole"도 나와 함께 괜찮을 것입니다.
kabanus

2

이것은 Clang 컴파일러의 버그 인 것 같습니다. die()정적 함수가되는 함수 에 대한 강제가 없으면 제거 static하고 작성하십시오 inline.

#include <stdio.h>

inline void die(void) {
    while(1)
        ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

Clang 컴파일러로 컴파일 할 때 예상대로 작동하며 이식 가능합니다.

컴파일러 탐색기 (godbolt.org)-clang 9.0.0-O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB0_1
.Lstr:
        .asciz  "begin"

무엇에 대해 static inline?
SS Anne

1

다음은 저에게 효과적입니다.

#include <stdio.h>

__attribute__ ((optnone))
static void die(void) {
    while (1) ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

에서 godbolt

명시 적으로 Clang에게 하나의 함수가 무한 루프가 예상대로 방출되도록 최적화하지 말라고 지시합니다. 바라건대 특정 최적화를 모두 끄는 대신 선택적으로 비활성화하는 방법이 있기를 바랍니다. Clang은 여전히 ​​두 번째 코드를 생성하지 printf않습니다. 그것을 강제로하기 위해 내부 코드를 추가로 수정해야 main했습니다.

volatile int x = 0;
if (x == 0)
    die();

무한 루프 기능에 대한 최적화를 비활성화 한 다음 무한 루프가 조건부로 호출되는지 확인해야합니다. 실제 세계에서 후자는 거의 항상 그렇습니다.


1
printf루프가 실제로 영원히 진행되면 두 번째 를 생성 할 필요가 없습니다 .이 경우 두 번째는 printf실제로 도달 할 수 없으므로 삭제할 수 있기 때문입니다 . (Clang의 오류는 도달 불가능을 감지 한 다음 도달 불가능한 코드에 도달하도록 루프를 삭제하는 데 있습니다.)
nneonneo

GCC 문서 __attribute__ ((optimize(1)))는 있지만 clang 은이 를 지원되지 않는 것으로 간주합니다 ( godbolt.org/z/4ba2HM) . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes

0

적합한 구현은, 그리고 많은 실제적인 것들은 프로그램이 얼마나 오랫동안 실행될 수 있는지 또는 얼마나 많은 명령을 실행할 것인지에 대한 임의의 제한을 부과 할 수 있으며, 그러한 제한이 위반되거나 "있는 그대로"규칙에 따라 임의의 방식으로 행동합니다 -불가피하게 위반 될 것으로 판단되는 경우. 단, 이행 한계, 한계의 존재, 문서화되는 범위 및 한계를 초과하지 않는 한도 내에서 N1570 5.2.4.1에 나열된 모든 한계를 명목상으로 수행하는 하나 이상의 프로그램을 구현에서 성공적으로 처리 할 수있는 경우 표준 관할권 밖의 모든 이행 품질 문제.

표준의 의도는 컴파일러가 while(1) {}부작용이나 break문장이 없는 루프 가 종료 될 것이라고 가정해서는 안된다는 것이 분명하다고 생각합니다 . 일부 사람들의 생각과는 달리, 표준 저자는 컴파일러 작성자를 어리 석거나 모호하게 초대하지 않았습니다. 적합한 구현은 중단되지 않는 한 우주에 원자보다 더 많은 부작용이없는 명령을 실행하는 프로그램을 종료하기로 결정하는 것이 유용 할 수 있지만, 품질 구현은에 대한 가정에 근거하여 그러한 조치를 수행해서는 안됩니다. 종료하지만 오히려 그렇게하는 것이 유용 할 수 있으며 (clang의 행동과 달리) 쓸모없는 것보다 나쁘지 않을 것이라는 점을 기초로합니다.


-2

루프에는 부작용이 없으므로 최적화 할 수 있습니다. 루프는 사실상 제로 작업 단위의 무한 반복입니다. 이것은 수학과 논리에 정의되어 있지 않으며 표준은 각 작업을 0 시간 안에 수행 할 수 있다면 구현이 무한한 수의 일을 완료 할 수 있는지 여부를 말하지 않습니다. Clang의 해석은 무한대 시간을 무한대가 아닌 0으로 처리하는 데 완벽하게 합리적입니다. 루프의 모든 작업이 실제로 완료되면 표준은 무한 루프가 종료 될 수 있는지 여부를 말하지 않습니다.

컴파일러는 표준에 정의 된대로 관찰 할 수없는 동작을 최적화 할 수 있습니다. 여기에는 실행 시간이 포함됩니다. 루프가 최적화되지 않은 경우 무한한 시간이 걸린다는 사실을 보존 할 필요는 없습니다. 이를 훨씬 더 짧은 런타임으로, 실제로는 대부분의 최적화 시점으로 변경할 수 있습니다. 루프가 최적화되었습니다.

clang이 코드를 순진하게 번역하더라도 이전 반복에 걸리는 시간의 절반으로 각 반복을 완료 할 수있는 최적화 CPU를 상상할 수 있습니다. 그것은 문자 그대로 무한한 시간 안에 무한 루프를 완성 할 것입니다. 이러한 최적화 CPU가 표준을 위반합니까? 최적화에 CPU가 너무 좋지 않으면 최적화 CPU가 표준을 위반한다고 말하는 것은 터무니없는 것 같습니다. 컴파일러도 마찬가지입니다.


의견은 긴 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .
Samuel Liew

4
당신이 (당신의 프로필에서) 경험을 보았을 때 나는이 게시물이 컴파일러를 방어하기 위해 악의로 작성되었다고 결론 내릴 수 있습니다. 무한한 시간이 걸리는 것을 반으로 실행하도록 최적화 할 수 있다고 진지하게 주장하고 있습니다. 그것은 모든 수준에서 어리 석고 당신도 알고 있습니다.
파이프

@pipe : clang과 gcc의 관리자는 향후 표준 버전이 컴파일러의 동작을 허용 할 것으로 기대하고 있으며, 이러한 컴파일러의 관리자는 그러한 변화가 단순히 오랫동안 결함을 수정 한 것처럼 가장 할 수 있다고 생각합니다 표준에서. 그것이 그들이 예를 들어 C89의 공통 초기 시퀀스 보증을 처리 한 방법입니다.
supercat

@ SSAnne : 흠 ... 나는 포인터 평등 비교 결과에서 gcc와 clang이 들리지 않는 일부 추론을 차단하기에 충분하지 않다고 생각합니다.
supercat

@supercat <s> 다른 </ s> 톤이 있습니다.
SS Anne

-2

이것이 사실이 아닌 경우 미안합니다.이 게시물을 우연히 발견했으며 Gentoo Linux 배포판을 사용하는 몇 년 동안 컴파일러가 코드를 최적화하지 않으려면 -O0 (Zero)를 사용해야한다는 것을 알고 있습니다. 나는 그것에 대해 궁금해하고 위의 코드를 컴파일하고 실행했으며 루프는 무한정 진행됩니다. clang-9를 사용하여 컴파일 :

cc -O0 -std=c11 test.c -o test

1
요점은 최적화가 활성화 된 무한 루프를 만드는 것입니다.
SS Anne

-4

while루프는 시스템에 부작용이 없습니다.

따라서 Clang은이를 제거합니다. 의도 한 행동을 달성하기위한 "더 나은"방법이 있습니다.

while(1); baaadd입니다.


6
많은 임베디드 구조에는 abort()또는 개념이 없습니다 exit(). 함수가 (아마도 메모리 손상의 결과로) 계속 실행이 위험보다 더 나쁘다고 판단하는 상황이 발생하면 임베디드 라이브러리의 일반적인 기본 동작은를 수행하는 함수를 호출하는 것입니다 while(1);. 컴파일러가 보다 유용한 동작 을 대체 할 수있는 옵션 을 갖는 것이 유용 할 수 있지만, 그러한 간단한 구성을 지속적인 프로그램 실행의 장벽으로 취급하는 방법을 알아낼 수없는 컴파일러 작성자는 복잡한 최적화를 신뢰할 수 없습니다.
supercat

당신의 의도를보다 명확하게 표현할 수있는 방법이 있습니까? 옵티마이 저는 프로그램을 최적화하기 위해 존재하며, 아무것도하지 않는 중복 루프를 제거하면 최적화됩니다. 이것은 실제로 수학 세계에 대한 추상적 사고와 적용되는 공학 세계 사이의 철학적 차이입니다.
유명한 Jameis

대부분의 프로그램에는 가능할 때 수행해야하는 유용한 조치와 사용하지 않는 최악의 조치가 있습니다. 많은 프로그램은 특정한 경우에 허용되는 동작 세트를 가지고 있는데, 그 중 하나는 실행 시간을 관찰 할 수없는 경우 항상 "임의의 임의 대기 후 세트에서 일부 조치 수행"입니다. 대기 이외의 모든 조치가 쓸모없는 최악의 조치 세트에있는 경우 "영원히 대기"가 다음과 같이
현저히

... "N + 1 초 기다렸다가 다른 조치를 수행"하므로 대기 이외의 허용 가능한 조치 세트가 비어 있다는 사실은 관찰 할 수 없습니다. 다른 한편으로, 코드 조각이 일련의 가능한 조치에서 허용 할 수없는 조치를 제거하고 해당 조치 중 하나가 어쨌든 수행되면 관찰 가능한 것으로 간주되어야합니다. 불행히도, C와 C ++ 언어 규칙은 내가 알아볼 수있는 다른 논리 나 인간의 노력과는 달리 "가정"이라는 단어를 이상한 방식으로 사용합니다.
supercat

1
@FamousJameis는 괜찮지 만 Clang은 루프를 제거하지 않고 나중에 모든 것을 도달 할 수없는 것으로 정적으로 분석하고 잘못된 명령을 내 보냅니다. 루프를 "제거"하면 예상 한 결과가 아닙니다.
nneonneo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.