programing

다른 최적화 수준이 기능적으로 다른 코드로 이어질 수 있습니까?

telecom 2023. 7. 19. 21:09
반응형

다른 최적화 수준이 기능적으로 다른 코드로 이어질 수 있습니까?

나는 컴파일러가 최적화할 때 갖는 자유도가 궁금합니다.이 질문을 GCC 및 C/C++(모든 버전, 모든 종류의 표준)로 제한합니다.

어떤 최적화 수준으로 컴파일되었는지에 따라 다르게 동작하는 코드를 작성할 수 있습니까?

제가 염두에 두고 있는 예는 C++의 다양한 생성자에서 다양한 텍스트를 인쇄하고 복사본이 삭제되는지 여부에 따라 차이를 얻는 것입니다(그런 것을 작동시키지는 못했지만).

클럭 주기는 계산할 수 없습니다.GCC가 아닌 컴파일러의 예시가 있다면 저도 궁금하겠지만 확인이 안 됩니다.C.의 예에 대한 보너스 포인트 :-)

편집: 예제 코드는 표준을 준수해야 하며 처음부터 정의되지 않은 동작을 포함해서는 안 됩니다.

편집 2: 이미 좋은 답변을 받았습니다!제가 판돈을 조금 올리겠습니다.코드는 잘 형성된 프로그램을 구성하고 표준을 준수해야 하며, 모든 최적화 수준에서 정확하고 결정론적인 프로그램을 컴파일해야 합니다. (잘못 형성된 멀티 스레드 코드의 레이스 조건과 같은 것은 제외됩니다.)부동소수점 라운딩이 영향을 받을 수 있다는 점도 감사하지만, 그것은 할인해 드리겠습니다.

저는 방금 800명의 명성을 얻었기 때문에, 저는 그 조건들의 (정신)에 부합하기 위해 첫 번째 완전한 예에 50명의 명성을 날려버릴 것이라고 생각합니다; 만약 그것이 엄격한 에일리어싱을 남용하는 것과 관련이 있다면. (누군가가 다른 사람에게 어떻게 현상금을 보내는지 보여주는 것에 따라)

적용되는 C++ 표준의 부분은 제1.9조 "프로그램 실행"입니다.부분적으로는 다음과 같습니다.

준수 구현은 아래 설명된 추상 기계의 관찰 가능한 동작을 에뮬레이트(만)하기 위해 필요합니다. ...

잘 형성된 프로그램을 실행하는 적합한 구현은 동일한 프로그램과 동일한 입력을 가진 추상 기계의 해당 인스턴스의 가능한 실행 시퀀스 중 하나와 동일한 관찰 가능한 동작을 생성해야 합니다. ...

추상 시스템의 관찰 가능한 동작은 휘발성 데이터에 대한 읽기 및 쓰기 시퀀스와 라이브러리 I/O 함수에 대한 호출입니다. ...

따라서 코드는 최적화 수준에 따라 다르게 동작할 수 있지만 (모든 수준이 적합한 컴파일러를 생성한다고 가정하면) 관찰 가능한 다르게 동작할 수 없습니다.

편집: 결론을 수정합니다.예, 각 동작이 표준 추상 기계의 동작 중 하나와 눈에 띄게 동일한 한 코드는 서로 다른 최적화 수준에서 다르게 동작할 수 있습니다.

부동 소수점 계산은 차이의 성숙한 원천입니다.개별 작업의 순서에 따라 반올림 오차가 더 많거나 더 적을 수 있습니다.

안전하지 않은 멀티스레드 코드는 메모리 액세스가 최적화되는 방식에 따라 다른 결과를 가져올 수도 있지만, 어쨌든 그것은 본질적으로 코드의 버그입니다.

그리고 당신이 언급했듯이, 최적화 수준이 변경되면 복사 생성자의 부작용이 사라질 수 있습니다.

어떤 최적화 수준으로 컴파일되었는지에 따라 다르게 동작하는 코드를 작성할 수 있습니까?

컴파일러 버그를 트리거한 경우에만 해당됩니다.

편집

이 예제는 gcc 4.5.2에서 다르게 작동합니다.

void foo(int i) {
  foo(i+1);
}

main() {
  foo(0);
}

으로 -O0분할 오류와 충돌하는 프로그램을 만듭니다.
으로 -O2무한 루프로 들어가는 프로그램을 만듭니다.

자, 구체적인 사례를 제공함으로써 현상금을 위한 저의 노골적인 플레이를 보여드리겠습니다.저는 다른 사람들의 답변과 제 의견을 종합하겠습니다.

수준에 A는 "최적화 수준 A"를 의미합니다.gcc -O0 4되지 . 제가 것이라고 합니다.) B는 (Optimization Level B)를 나타내야 .gcc -O0 -fno-elide-constructors.

코드는 간단합니다.

#include <iostream>

struct Foo {
    ~Foo() { std::cout << "~Foo\n"; }
};

int main() {
    Foo f = Foo();
}

최적화 레벨 A에서의 출력:

~Foo

최적화 레벨 B에서의 출력:

~Foo
~Foo

코드는 완전히 합법적이지만, 복사 생성자 삭제로 인해 출력은 구현에 따라 달라지며, 특히 복사자 삭제를 비활성화하는 gcc의 최적화 플래그에 민감합니다.

일반적으로 "최적화"는 정의되지 않은 동작, 지정되지 않은 동작 또는 구현 정의된 동작을 변경할 수 있지만 표준에 의해 정의된 동작은 변경할 수 없는 컴파일러 변환을 의미합니다.따라서 당신의 기준을 만족시키는 모든 예는 출력이 지정되지 않았거나 구현이 정의된 프로그램입니다.이 경우 복사기가 삭제되는지 여부는 표준에 의해 지정되지 않습니다. GCC가 허용될 때마다 거의 안정적으로 삭제하지만 비활성화할 수 있는 옵션이 있다는 것이 행운입니다.

C의 경우 거의 모든 작업이 추상 시스템에서 엄격하게 정의되며, 관측 가능한 결과가 추상 시스템의 결과와 정확히 일치하는 경우에만 최적화가 허용됩니다.생각나는 그 규칙의 예외:

  • 정의되지 않은 동작은 서로 다른 컴파일러 실행 또는 결함 있는 코드의 실행 간에 일관될 필요가 없습니다.
  • 부동 소수점 연산으로 인해 다른 반올림이 발생할 수 있음
  • 함수 호출에 대한 인수는 임의의 순서로 평가할 수 있습니다.
  • 가 표현volatile 않을 도 있습니다.
  • const 복합 메모리 않을 수도 .

표준에 따라 정의되지 않은 동작은 최적화 수준(또는 달 위상)에 따라 동작을 변경할 수 있습니다.

복사 생성자 호출은 최적화될 수 있기 때문에 부작용이 있더라도 복사 생성자가 있으면 최적화되지 않은 코드와 최적화된 코드가 다르게 동작합니다.

-fstrict-aliasing동일한 메모리 블록에 대한 두 개의 포인터가 있는 경우 옵션을 사용하면 동작이 쉽게 변경될 수 있습니다.이것은 무효라고 생각되지만 실제로 꽤 흔한 일입니다.

이 C 프로그램은 정의되지 않은 동작을 호출하지만 서로 다른 최적화 수준에서 서로 다른 결과를 표시합니다.

#include <stdio.h>
/*
$ for i in 0 1 2 3 4 
    do echo -n "$i: " && gcc -O$i x.c && ./a.out 
  done
0: 5
1: 5
2: 5
3: -1
4: -1
*/

void f(int a) {
  int b;
  printf("%d\n", (int)(&a-&b));
}
int main() {
 f(0);
 return 0;
}

는 gcc를 정의합니다.__OPTIMIZE__0이 아닌 최적화 수준이 사용되는 경우 매크로.아래와 같이 사용할 수 있습니다.

#ifdef __OPTIMIZE__
printf("Code compiled with -O1 or higher\n");
#else
printf("Code compiled with -O0\n");
#endif

같은 소스 코드

enable -finline-small-small-small-small-small-small-small-small

Before enable -finline-small-functions

After enable -finline-small-functions

-finline-small-function은 -O2/-O3에서 활성화할 수 있습니다.

두 가지 다른 C 프로그램:

foo6.c

void p2(void);

int main() {
    p2();
    return 0;
}

bar6.c

#include <stdio.h>

char main;

void p2() {
    printf("0x%x\n", main);
}

두 모듈을 최적화 레벨 1과 0으로 하나의 실행 가능한 모듈로 컴파일하면 두 개의 서로 다른 값이 출력됩니다. -O1의 경우 0x48, -O0의 경우 0x55

터미널 스크린샷

다음은 내 환경에서 작동하는 예입니다.

AC:

char *f1(void) { return "hello"; }

b.c:

#include <stdio.h>

char *f1(void);

int main()
{
    if (f1() == "hello") printf("yes\n");
        else printf("no\n");
}

출력은 병합 문자열 상수 최적화의 사용 여부에 따라 달라집니다.

ac a ./agcc a.c b.c -o a -fno-deline-deline; .
아니요.
ac a ./agcc a.c b.c -o a -fmerge-c; ./a
, 그렇습니다.

오늘 제 OS 과정에서 흥미로운 사례를 몇 가지 얻었습니다.컴파일러가 병렬 실행에 대해 모르기 때문에 최적화 시 손상될 수 있는 일부 소프트웨어 뮤텍스를 분석했습니다.

컴파일러는 종속 데이터에서 작동하지 않는 문을 다시 정렬할 수 있습니다.이미 병렬화된 코드로 기술했듯이 컴파일러에 대한 의존성은 숨겨져서 손상될 수 있습니다.제가 제시한 예는 스레드 안전성이 손상되고 OS 스케줄링 문제와 동시 액세스 오류로 인해 코드가 예측 불가능하게 동작하기 때문에 디버깅에 어려움을 겪을 수 있습니다.

언급URL : https://stackoverflow.com/questions/6364365/can-different-optimization-levels-lead-to-functionally-different-code

반응형