source

2-D 배열 앨리어싱 시 예기치 않은 스트렐렌 최적화

manysource 2023. 11. 1. 22:27

2-D 배열 앨리어싱 시 예기치 않은 스트렐렌 최적화

여기 내 코드가 있습니다.

#include <string.h>
#include <stdio.h>

typedef char BUF[8];

typedef struct
{
    BUF b[23];
} S;

S s;

int main()
{
    int n;

    memcpy(&s, "1234567812345678", 17);

    n = strlen((char *)&s.b) / sizeof(BUF);
    printf("%d\n", n);

    n = strlen((char *)&s) / sizeof(BUF);
    printf("%d\n", n);
}

gcc 8.3.0 또는 8.2.1을 제외한 모든 최적화 레벨에서 사용-O0, 이 산출물들0 2내가 예상하고 있던 때에2 2. 컴파일러는 다음과 같이 결정했습니다.strlen에 국한됩니다.b[0]따라서 분할되는 값과 같거나 초과할 수 없습니다.

이것은 제 코드의 버그인가요, 컴파일러의 버그인가요?

이것은 표준에 명확하게 명시되어 있지는 않지만, 포인터 프로번스에 대한 주류 해석은 어떤 물체에 대해서도X, 강령(char *)&X전체적으로 반복할 수 있는 포인터를 생성해야 합니다.X-- 이 개념은 비록 유지되야 합니다.X내부 구조로 하위 arrays을 갖게 됩니다

(보너스 질문, 이 특정 최적화를 끄는 gcc 플래그가 있습니까?)

이거 확인해보니까 이거랑 같이.-O1gcc 8.3에 있어서 방금 여기 gcc 최적화 플래그 목록을 열고 하나씩 실험을 시작했습니다.다음과 같이 희소 조건부 상수 전파만 비활성화하는 것으로 나타났습니다.-fno-tree-ccp문제를 사라지게 만들었습니다(오 운이 좋네요, 하나씩 테스트해도 결과가 나오지 않으면 몇 개의 플래그를 테스트하려고 계획했습니다).

그 다음에 저는 다음으로 바꿨습니다.-O2지우지 않았습니다.-fno-tree-ccp깃발. 또 재현이 됐어요."오케이" 라고 말하고 추가적인 테스트를 시작했습니다.-O2flags. 단일 Value Range Propagation(값 범위 전파)을 비활성화하면 추가적으로 의도된 결과를 초래하는 것으로 나타났습니다.2 2산출량.그런 다음에 그걸 먼저 지웠습니다.-fno-tree-ccp하지만 다시 번식하기 시작했습니다.그래서.-O2지정할 수 있습니다.-O2 -fno-tree-ccp -fno-tree-vrp당신의 프로그램이 기대한 대로 작동하도록 하기 위해서입니다.

이 플래그를 지우지 않고 다음으로 전환했습니다.-O3문제가 재현되지 않았습니다.

따라서 gcc 8.3의 이 두 가지 최적화 기술 모두 이러한 이상한 동작을 초래합니다(내부적으로 일반적인 것을 사용할 수도 있음).

  • 트리에서의 희박 조건부 상수 전파
  • 트리에서의 값 범위 전파

저는 거기서 무슨 일이 일어나고 있는지, 왜 일어나고 있는지, 다른 사람이 설명할 수 있을지도 모릅니다.하지만 확실히 특정할 수 있습니다.-fno-tree-ccp -fno-tree-vrp플래그를 지정하여 코드가 예상대로 작동하도록 이러한 최적화 기법을 비활성화합니다.

"일을 열심히 할수록 운이 좋아집니다." – Samuel Goldwyn

편집

@KamilCuk이 문제의 댓글에서 언급했듯이,-fno-builtin-strlen또한 의도된 동작으로 이어지므로 데드 코드를 차단하고 가능한 식 값을 정적으로 결정하고 프로그램을 통해 상수를 전파하는 기본 제공다른 최적화를 결합한 컴파일러 버그가 있을 가능성이 높습니다.컴파일러는 구현 시 문자열 길이를 데드 코드(정수 분할 및/또는 2차원 배열과 결합하여)로 결정하고 컴파일 시 0으로 계산하는 것을 가장 잘못 고려했을 것이라고 생각했습니다.그래서 저는 이론을 확인하고 버그의 다른 "참가자"들을 제거하기 위해 코드를 조금 가지고 놀기로 결정했습니다.제 생각을 확인해 준 행동의 아주 작은 예에 도달했습니다.

int main()
{
    // note that "7" - inner arrays size, you can put any other number here
    char b[23][7]; // local variable, no structs, no typedefs
    memcpy(&b[0][0], "12345678123456781234", 21);

    printf("%d\n", strlen(&b[0][0]) / 8); // greater than that "7" !!!
    printf("%d\n", strlen(&b[0][0]) / 7);
    printf("%d\n", strlen(&b[0][0]) / 6); // less than that "7" !!!
    printf("%d\n", strlen(&b[0][0])); // without division
}

0

0

3

20

우리는 이것을 gcc의 버그라고 생각할 수 있을 것 같습니다.

생각합니다-fno-builtin-strlen모든 최적화 레벨에만 적용되고 내장되어 있기 때문에 문제에 대한 더 나은 해결책입니다.strlen특히 당신의 프로그램이 사용하지 않는 경우에는, 덜 강력한 최적화 기술인 것 같습니다.strlen()많이. 그래도.-fno-tree-ccp -fno-tree-vrp는 옵션이기도 합니다.

구조를 정의하는 방식은 처음에는 배열 유형을 만들어 본 적이 없는 것 같아서 혼란스러웠습니다.기능에 전달하려고 하면 값을 전달한다고 생각할 수 있지만 실제로는 기준을 전달할 수 있기 때문에 그런 식으로 하는 것도 위험할 수 있습니다.스타일에 상관없이, 만약 내가 그런 타입을 만들어야 한다면, 나는 다음과 같은 것을 할 것입니다.

//typedef char BUF[8];

//do it this way instead
typedef struct
{
    char x[8];
} BUF;

typedef struct
{
    BUF b[23];
} S;

그런 식으로 정의하면 어느 쪽이든 기대 값을 반환합니다.여기서 보세요.

컴파일러가 메모리 레이아웃을 결정하는 방식에 따라 영향을 받을 수 있는 몇 가지 문제가 있습니다.

    n = strlen((char *)&s.b) / sizeof(BUF);
    printf("%d\n", n);

위 코드에서s.b는 8자 배열로 구성된 23개 엔트리 배열입니다.당신이 단지 언급할 때.s.b23 바이트 배열의 첫 번째 항목(그리고 8 문자 배열의 첫 번째 바이트)의 주소를 가져옵니다.코드에 의하면&s.b, 이것은 배열의 주소를 묻는 것입니다.표지 아래에서 컴파일러는 일부 로컬 스토리지를 생성하여 어레이의 주소를 저장하고 로컬 스토리지의 주소를 제공할 가능성이 높습니다.strlen.

두 가지 가능한 해결책이 있습니다.다음과 같습니다.

    n = strlen((char *)s.b) / sizeof(BUF);
    printf("%d\n", n);

아니면

    n = strlen((char *)&s.b[0]) / sizeof(BUF);
    printf("%d\n", n);

나는 또한 당신의 프로그램을 실행하고 그 문제를 시연하려고 했지만, 내가 가지고 있는 gcc 버전과 clang 둘다.-O옵션은 여전히 당신이 기대한 대로 작동했습니다.제가 x86_64-pc-linux-gnu)에서 clang 버전 9.0.0-2와 gcc 버전 9.2.1을 실행하고 있습니다.

이것은 gcc의 버그일 수도 있다고 생각합니다.몇 가지 해결책을 찾았지만, 가장 쉬운 것은 noinline 속성으로 프록시 함수를 만드는 것 같습니다.그러면 다른 최적화를 놓치는 것이 아니라 스트렌과 관련된 최적화를 놓치는 것입니다.

int  __attribute__ ((noinline)) _strlen(char *x) { return strlen(x); }
#define strlen _strlen

int main(){
    int n;

    memcpy(&s, "1234567812345678", 17);
    n = strlen((char *)&s.b) / sizeof(BUF);
    printf("%d\n", n);

    n = strlen((char *)&s) / sizeof(BUF);
    printf("%d\n", n);
}

컴파일러 탐색기에서 출력을 볼 수 있습니다.https://godbolt.org/z/U2L9us

코드에 오류가 있습니다.

 memcpy(&s, "1234567812345678", 17);

예를 들어, 위험합니다. b로 시작하는 s는 다음과 같습니다.

 memcpy(&s.b, "1234567812345678", 17);

두번째 strlen()에도 오류가 있습니다.

n = strlen((char *)&s) / sizeof(BUF);

예를 들어 다음과 같습니다.

n = strlen((char *)&s.b) / sizeof(BUF);

s.b 문자열은 올바르게 복사된 경우 17자 길이여야 합니다.구조물이 정렬되어 있는 경우 메모리에 어떻게 저장되어 있는지 확실하지 않습니다.s.b에 실제로 복사된 17자가 포함되어 있는지 확인했습니까?

그러므로 스트렐렌(s.b)은 17을 보여야 합니다.

%d이(가) 정수이고 변수 n은 정수로 선언되므로 printf에는 정수 숫자만 표시됩니다. 크기는 (BUF) 8이어야 합니다.

따라서 17을 8로 나눈 값(17/8)은 n이 정수로 선언됨에 따라 2를 인쇄해야 합니다.memcpy는 s로 데이터를 복사하고 s.b로 복사하지 않기 위해 사용되었기 때문에 메모리 정렬과 관련이 있다고 생각합니다. 64비트 컴퓨터라고 가정하면 하나의 메모리 주소에 8자가 있을 수 있습니다.

예를 들어, 어떤 사람이 malloc(1)을 불렀다고 가정해 봅시다. 다음 "자유 공간"이 정렬되지 않은 경우...

두 번째 strlen 호출은 문자열 복사가 s.b 대신 struct에 수행되었기 때문에 정확한 번호를 보여줍니다.

언급URL : https://stackoverflow.com/questions/58759591/unexpected-optimization-of-strlen-when-aliasing-2-d-array