엄격한 앨리어싱 규칙은 무엇입니까?


803

C에서 정의되지 않은 일반적인 동작에 대해 질문 할 때 때때로 엄격한 앨리어싱 규칙을 참조합니다.
그들은 무엇에 대한 이야기?


12
@Ben Voigt : 앨리어싱 규칙은 c ++와 c에 따라 다릅니다. 왜 태그이 질문 c하고 c++faq.
MikeMB

6
@ MikeMB : 기록을 확인하면 다른 전문가가 기존 답변 아래에서 질문을 변경하려는 시도에도 불구하고 태그를 원래대로 유지 한 것을 알 수 있습니다. 게다가 언어 의존성과 버전 의존성은 "엄격한 앨리어싱 규칙은 무엇입니까?"에 대한 답변의 매우 중요한 부분입니다. C와 C ++간에 코드를 마이그레이션하거나 두 가지 용도로 매크로를 작성하는 팀에게는 차이점을 아는 것이 중요합니다.
Ben Voigt

6
@ Ben Voigt : 실제로-내가 알 수있는 한 대부분의 답변은 c에만 관련이 있고 c ++에는 관련이 없으며 질문의 문구는 C 규칙에 초점을 나타냅니다 (또는 OP는 인식하지 못했습니다. 차이가 있음) ). 대부분의 경우 규칙과 일반적인 아이디어는 물론 동일하지만 특히 노조가 우려되는 경우 C ++에는 대답이 적용되지 않습니다. 일부 C ++ 프로그래머는 엄격한 앨리어싱 규칙을 찾고 여기에 언급 된 모든 내용이 c ++에도 적용된다고 가정 할 것입니다.
MikeMB

반면에, 많은 정답이 게시 된 후에 질문을 변경하는 데 문제가 있으며 문제는 사소한 것임을 동의합니다.
MikeMB

1
@ MikeMB : C ++에 맞지 않게 받아 들인 대답에 C 초점이 타사에 의해 편집 된 것을 볼 수 있습니다. 이 부분은 아마도 다시 수정되어야합니다.
벤 Voigt

답변:


562

엄격한 앨리어싱 문제가 발생하는 일반적인 상황은 구조체 (예 : 장치 / 네트워크 메시지)를 시스템의 단어 크기 버퍼 ( uint32_ts 또는 uint16_ts에 대한 포인터)에 오버레이하는 경우 입니다. 포인터를 캐스팅하여 이러한 버퍼 또는 구조체에 버퍼를 오버레이하면 엄격한 앨리어싱 규칙을 쉽게 위반할 수 있습니다.

따라서 이런 종류의 설정에서 메시지를 보내려면 동일한 메모리 청크를 가리키는 두 개의 호환되지 않는 포인터가 있어야합니다. 그런 다음 순진하게 다음과 같은 시스템을 코딩 할 수 있습니다 sizeof(int) == 2.

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

엄격한 앨리어싱 규칙은이 설정을 불법으로 만듭니다. 호환 가능한 유형 이 아니 거나 C 2011 6.5 단락 7 1에서 허용하는 다른 유형 중 하나 가 아닌 개체의 별칭을 지정하는 포인터 는 정의되지 않은 동작입니다. 불행히도, 당신은 여전히 ​​이런 식으로 코딩 할 수 있습니다. 어쩌면 경고를 받고, 잘 컴파일되도록하고, 코드를 실행할 때 이상한 예기치 않은 동작이 발생합니다.

(GCC는 앨리어싱 경고를 제공하는 기능이 다소 일관성이없는 것으로 보이며 때로는 친숙한 경고를 제공하지만 때로는 그렇지 않습니다.)

이 동작이 정의되지 않은 이유를 확인하려면 엄격한 앨리어싱 규칙이 컴파일러를 구입하는 것에 대해 생각해야합니다. 기본적으로이 규칙을 사용하면 buff모든 루프 실행 내용을 새로 고치기 위해 명령어 삽입에 대해 생각할 필요가 없습니다 . 대신, 앨리어싱에 대해 성가 시게 강요되지 않는 가정을 사용하여 최적화 할 때 , 루프가 실행되기 전에 해당 명령어를 생략 하고 CPU 레지스터에 로드 buff[0]buff[1]하고 루프 본문을 가속화 할 수 있습니다. 엄격한 앨리어싱이 도입되기 전에 컴파일러는 그 내용이 buff언제 어디서나 변경 될 수 있는 편집증 상태에 있어야했습니다 . 따라서 성능을 높이고 대부분의 사람들이 포인터를 입력하지 않는다고 가정하면 엄격한 앨리어싱 규칙이 도입되었습니다.

예제가 고안되었다고 생각하면 버퍼를 대신 보내는 다른 함수에 버퍼를 전달하는 경우에도 발생할 수 있습니다.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

이 편리한 기능을 활용하기 위해 이전 루프를 다시 작성했습니다.

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

컴파일러는 SendMessage를 인라인하려고 시도하기에 충분하거나 현명하지 않을 수 있으며, 다시 버프를로드하거나로드하지 않기로 결정할 수도 있습니다. SendMessage별도로 컴파일 된 다른 API의 일부인 경우 에는 버프 내용을로드하라는 지침이있을 수 있습니다. 그런 다음 C ++에있을 수 있으며 컴파일러가 인라인 할 수 있다고 생각하는 템플릿 화 된 헤더 전용 구현입니다. 또는 자신의 편의를 위해 .c 파일에 작성한 것일 수도 있습니다. 어쨌든 정의되지 않은 동작이 계속 발생할 수 있습니다. 우리가 어떤 일이 벌어지고 있는지 아는 경우에도 여전히 규칙을 위반하므로 잘 정의 된 동작이 보장되지 않습니다. 따라서 단어 구분 버퍼를 취하는 함수를 래핑한다고해서 반드시 도움이되는 것은 아닙니다.

어떻게이 문제를 해결할 수 있습니까?

  • 노조를 사용하십시오. 대부분의 컴파일러는 엄격한 앨리어싱에 대해 불평하지 않고이를 지원합니다. 이것은 C99에서 허용되고 C11에서 명시 적으로 허용됩니다.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • 컴파일러에서 엄격한 앨리어싱을 비활성화 할 수 있습니다 ( gcc에서 f [no-] strict-aliasing ))

  • char*시스템 단어 대신 앨리어싱에 사용할 수 있습니다 . 규칙은 char*( signed char및 포함 unsigned char)에 대한 예외를 허용합니다 . 항상 char*다른 유형의 별명으로 가정합니다 . 그러나 이것은 다른 방식으로는 작동하지 않습니다. 구조체 별칭에 문자 버퍼가 있다고 가정하지 않습니다.

초보자 조심

이것은 두 가지 유형을 서로 오버레이 할 때 하나의 잠재적 인 지뢰밭입니다. 엔디안 , 단어 정렬구조체를 올바르게 패킹 하여 정렬 문제를 처리하는 방법 에 대해서도 배워야 합니다.

각주

1 C 2011 6.5 7에서 lvalue가 액세스 할 수있는 유형은 다음과 같습니다.

  • 객체의 유효 유형과 호환되는 유형
  • 유효 객체 유형과 호환되는 유형의 정규화 된 버전
  • 객체의 유효 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • 오브젝트의 유효 유형의 규정 된 버전에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • 상기 구성원들 중 상기 유형들 중 하나를 포함하는 집합 또는 연합 유형 (재귀 적으로 하위 집합의 구성원 또는 포함 된 연합을 포함) 또는
  • 문자 유형

16
나는 전투 후에오고 unsigned char*있다 .. char*대신에 멀리 사용될 지도 모른다 ? 내가 사용하는 경향 unsigned char보다는 char위한 기본 유형으로 byte내 바이트가 서명하지 않기 때문에 내가 (특히 오버 플로우 WRT) 서명 행동의 불확실성을 원하지 않는다
마티유 M.

30
@ Matthieu : Signedness는 별칭 규칙과 아무런 차이가 없으므로 사용하는 unsigned char *것이 좋습니다.
Thomas Eding 2016 년

22
마지막으로 작성된 것과 다른 조합 멤버에서 읽는 것이 정의되지 않은 동작입니까?
R. Martinho Fernandes

23
Bollocks,이 답변은 완전히 거꾸로 입니다. 불법으로 표시되는 예는 실제로 합법적이며 실제로 표시되는 예는 실제로 불법입니다.
R. Martinho Fernandes

7
귀하 uint32_t* buff = malloc(sizeof(Msg));와 후속 통합 unsigned int asBuffer[sizeof(Msg)];버퍼 선언의 크기는 다르며 정확하지 않습니다. malloc전화는 버그 나 없음-the- (그것을하지 않는 것) 노조가 필요 이상 4 배 더 큰 것입니다 ... 나는 그것이 명확성을 위해 것을 이해하지만, 후드 4 바이트 정렬에 의존 less ...
nonsensickle

233

내가 찾은 가장 좋은 설명은 Mike Acton의 엄격한 앨리어싱 이해 입니다. PS3 개발에 중점을 두었지만 기본적으로 GCC입니다.

기사에서 :

"엄격한 앨리어싱은 C (또는 C ++) 컴파일러에 의해 만들어진 것으로, 다른 유형의 객체에 대한 포인터를 역 참조하는 것은 결코 같은 메모리 위치를 참조하지 않을 것이라고 가정합니다."

기본적으로 만약 당신이 int*어떤 메모리를 가리키는 메모리를 가리키고 있다면 inta float*를 그 메모리 를 가리키고 float규칙을 어길 때 사용하십시오 . 코드가 이것을 존중하지 않으면 컴파일러의 최적화 프로그램이 코드를 손상시킬 가능성이 높습니다.

규칙에 대한 예외는이며 char*모든 유형을 가리킬 수 있습니다.


6
그렇다면 두 가지 유형의 변수가있는 동일한 메모리를 합법적으로 사용하는 표준 방법은 무엇입니까? 아니면 모두 그냥 복사합니까?
jiggunjer

4
Mike Acton의 페이지에 결함이 있습니다. 최소한 "노동을 통한 주물 (2)"부분은 옳지 않다. 그가 법적이라고 주장하는 코드는 유효하지 않습니다.
davmac

11
@davmac : C89의 저자는 프로그래머가 농구대를 뛰어 넘도록 강요하지 않았습니다. 프로그래머가 최적화 프로그램이 중복 코드를 제거하기를 희망하여 중복 데이터를 복사하는 코드를 작성하도록 프로그래머가 요구하는 방식으로 최적화의 유일한 목적으로 존재하는 규칙을 해석해야한다는 개념을 철저히 기괴하게 생각합니다.
supercat

1
@ curiousguy : "노조를 가질 수 없습니다"? 첫째, 노조의 원래 / 주요 목적은 어떤 식 으로든 앨리어싱과 관련이 없습니다. 둘째, 현대 언어 사양에서는 앨리어싱을위한 공용체 사용을 명시 적으로 허용합니다. 컴파일러는 공용체가 사용되고 상황을 처리하는 것이 특별한 방법임을 알아야합니다.
AnT

5
@curiousguy : 거짓. 첫째, 노조의 기본 개념 은 주어진 노조 객체에 오직 하나의 멤버 객체 만이 존재하고 다른 것들은 존재하지 않는다는 것이었다 . 따라서 당신이 생각하는 것처럼 "같은 주소에 다른 물체"가 없습니다. 둘째, 모든 사람이 말하는 앨리어싱 위반 은 단순히 같은 주소 를 가진 두 개의 객체를 갖는 것이 아니라 하나의 객체를 다른 객체로 액세스하는 것 입니다. type-punning access 가 없으면 문제가 없습니다. 그것은 원래의 아이디어였습니다. 나중에 노조를 통한 타이핑이 허용되었습니다.
AnT

133

이것은 C ++ 03 표준 의 3.10 섹션에있는 엄격한 앨리어싱 규칙입니다 (다른 답변은 좋은 설명을 제공하지만 규칙 자체는 제공하지 않았습니다).

프로그램이 다음 유형 중 하나 이외의 lvalue를 통해 오브젝트의 저장된 값에 액세스하려고하면 동작이 정의되지 않습니다.

  • 객체의 동적 유형
  • 객체의 동적 유형의 cv-qualified 버전
  • 객체의 동적 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • cv-qualified 버전의 객체 동적 유형에 해당하는 부호있는 또는 부호없는 유형 인 유형
  • 상기 구성원들 중 상기 유형들 중 하나를 포함하는 집합 또는 연합 유형 (재귀 적으로 하위 집합 또는 포함 된 연합의 구성원 포함)
  • 객체의 다이나믹 타입의 (아마도 cv-qualified) 기본 클래스 타입 인 타입
  • char또는 unsigned char유형입니다.

C ++ 11C ++ 14 문구 (변경 사항 강조) :

프로그램 이 다음 유형 중 하나 이외 의 glvalue 를 통해 객체의 저장된 값에 액세스하려고 하면 동작이 정의되지 않습니다.

  • 객체의 동적 유형
  • 객체의 동적 유형의 cv-qualified 버전
  • 객체의 동적 유형과 유사한 유형 (4.4에 정의 된대로)
  • 객체의 동적 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • cv-qualified 버전의 객체 동적 유형에 해당하는 부호있는 또는 부호없는 유형 인 유형
  • 그 중에서, 상기 종류 중 하나를 포함하는 전체 또는 조합 형태 요소 또는 비 정적 데이터 멤버 (포함한 재귀 원소 또는 비 - 정적 데이터 부재 subaggregate 또는 연합 포함)
  • 객체의 다이나믹 타입의 (아마도 cv-qualified) 기본 클래스 타입 인 타입
  • char또는 unsigned char유형입니다.

lvalue 대신 glvalue 와 집계 / 연합 사례의 명확화 라는 두 가지 변경 사항이 작았습니다 .

세 번째 변경은보다 강력한 보증을 제공합니다 (강한 앨리어싱 규칙을 완화 함). 이제 별칭에 안전한 유사한 유형 의 새로운 개념 .


또한 C 문구 (C99; ISO / IEC 9899 : 1999 6.5 / 7; ISO / IEC 9899 : 2011 §6.5 ¶7에서 정확히 동일한 문구가 사용됨) :

객체는 다음 유형 73) 또는 88) 중 하나를 가진 lvalue 표현식으로 만 저장된 값에 액세스해야합니다 .

  • 객체의 유효 유형과 호환되는 유형
  • 객체의 유효 유형과 호환되는 유형의 정규화 된 버전
  • 객체의 유효 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • 객체의 유효 유형의 정규화 된 버전에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • 상기 구성원들 중 상기 유형들 중 하나를 포함하는 집합 또는 연합 유형 (재귀 적으로 하위 집합의 구성원 또는 포함 된 연합을 포함) 또는
  • 문자 유형

73) 또는 88) 이 목록의 목적은 개체의 별칭이 될 수도 있고 그렇지 않을 수도있는 환경을 지정하는 것입니다.


7
벤은 사람들이 종종 여기에 지시 할 때, 완전성을 위해 C 표준에 대한 참조를 추가하도록 허용했습니다.
Kos

1
이에 대해 이야기 하는 C89 이론적 cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf 섹션 3.3을보십시오.
phorgan1

2
구조 유형의 lvalue가 있고 구성원의 주소를 가져 와서이를 구성원 유형에 대한 포인터로 사용하는 함수에 전달하면 구성원 유형의 오브젝트에 액세스하는 것으로 간주됩니다 (법적). 또는 구조 유형의 객체 (금지)? 많은 코드는 같은 방식으로 액세스 구조에 합법적 가정, 나는 많은 사람들이 그런 행동을 금지하는 것으로 이해 된 규칙에 투덜 거림 것이라고 생각하지만 그것은 정확한 규칙이 무엇인지 불분명하다. 또한 노조와 구조는 동일하게 취급되지만 각각에 대한 합리적인 규칙은 달라야합니다.
supercat 2011

2
@supercat : 구조에 대한 규칙이 표현되는 방식으로 실제 액세스는 항상 기본 유형에 대한 것입니다. 그런 다음 기본 유형에 대한 참조를 통한 액세스는 유형이 일치하기 때문에 합법적이며, 포함 구조 유형에 대한 참조를 통한 액세스는 특별히 허용되므로 합법적입니다.
벤 Voigt

2
@ BenVoigt : 유니온을 통해 액세스가 수행되지 않으면 일반적인 초기 시퀀스가 ​​작동한다고 생각하지 않습니다. 참조 goo.gl/HGOyoK을 무엇을하고 있는지 GCC 볼 수 있습니다. 멤버 유형의 lvalue (union-member-access 연산자를 사용하지 않음)를 통해 유니온 유형의 lvalue에 액세스하는 것이 합법적 wow(&u->s1,&u->s2)이라면 포인터를 수정하는 데 사용되는 경우에도 합법적이어야하며 u, 이로 인해 대부분의 최적화가 무효화됩니다. 앨리어싱 규칙은 용이하게 설계되었습니다.
supercat

81

노트

이것은 "엄격한 앨리어싱 규칙은 무엇이며 왜 우리는 신경 쓰는가?" 에서 발췌 한 것 입니다. 쓰기.

엄격한 앨리어싱이란 무엇입니까?

C 및 C ++에서 앨리어싱은 저장된 값에 액세스 할 수있는 표현식 유형과 관련이 있습니다. C 및 C ++에서 표준은 어떤 표현식 유형이 어떤 유형의 별명을 지정할 수 있는지 지정합니다. 컴파일러와 옵티마이 저는 앨리어싱 규칙을 엄격하게 따른다고 가정 할 수 있으므로 엄격한 앨리어싱 규칙 이라는 용어가 사용 됩니다. 허용되지 않는 유형을 사용하여 값에 액세스하려고하면 정의되지 않은 동작 ( UB ) 으로 분류됩니다 . 우리가 정의되지 않은 행동을 취하면 모든 베팅이 해제되고, 프로그램 결과는 더 이상 신뢰할 수 없습니다.

불행히도 엄격한 앨리어싱 위반으로 인해 우리는 종종 예상 결과를 얻습니다. 새로운 최적화 기능을 갖춘 컴파일러의 향후 버전은 우리가 유효하다고 생각한 코드를 손상시킬 가능성을 남겨 둡니다. 이는 바람직하지 않으며 엄격한 앨리어싱 규칙과이를 위반하지 않는 방법을 이해하는 것이 좋습니다.

우리가 왜 관심을 갖는지에 대해 더 이해하기 위해, 엄격한 별칭 지정 규칙을 위반할 때 발생하는 문제, 유형 정리에 사용되는 일반적인 기술이 종종 엄격한 별칭 지정 규칙을 위반하기 때문에 유형 펀칭 및 펀칭 유형을 올바르게 입력하는 방법에 대해 논의합니다.

예비 예

몇 가지 예를 살펴보면 표준이 말하는 내용에 대해 자세히 이야기하고 몇 가지 추가 예를 검토 한 다음 엄격한 앨리어싱을 피하고 우리가 놓친 위반을 포착하는 방법을 볼 수 있습니다. 다음은 놀랍지 않아야 할 예제입니다 ( live example ).

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

우리는이 * INT 메모리에 포인팅가에 의해 점유 지능 이 유효한 앨리어싱이다. 옵티마이 저는 ip를 통한 할당 이 x가 차지하는 값을 업데이트 할 수 있다고 가정해야합니다 .

다음 예제는 정의되지 않은 동작으로 이어지는 앨리어싱을 보여줍니다 ( live example ).

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

foo 함수에서 우리는 int *float *를 취합니다 .이 예제에서 우리는 foo 를 호출 하고이 예제에서 int 를 포함하는 동일한 메모리 위치를 가리 키도록 두 매개 변수를 설정합니다 . 은 참고 reinterpret_cast는 그것의 템플릿 매개 변수에 의해 지정하신 유형을 가지고있는 것처럼 표현을 치료하기 위해 컴파일러을 말하고있다. 이 경우 표현식 & xfloat * 유형 인 것처럼 취급하도록 지시합니다 . 우리는 순진하게 두 번째 cout 의 결과 가 0 이 될 것으로 기대할 수 있지만 gcc와 clang 모두 -O2를 사용하여 최적화를 활성화 하면 다음과 같은 결과가 나타납니다.

0
1

정의되지 않은 동작을 호출했기 때문에 예상되지 않았지만 완벽하게 유효한 것입니다. 플로트 하지 유효 별명 할 수 INT의 객체입니다. 따라서 f를 통한 저장 이 int 객체에 유효하게 영향을 줄 수 없기 때문에 i 는 역 참조 할 때 i 가 반환 값이 될 때 저장된 상수 1을 가정 할 수 있습니다 . 컴파일러 탐색기에서 코드를 연결하면 이것이 정확히 무슨 일인지 알 수 있습니다 ( live example ).

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

TBAA (Type-Based Alias ​​Analysis)를 사용하는 옵티마이 저는 1 이 리턴되고 상수 값을 리턴 값을 전달하는 레지스터 eax 로 직접 이동 한다고 가정 합니다. TBAA는로드 및 저장을 최적화하기 위해 별명을 지정할 수있는 유형에 대한 언어 규칙을 사용합니다. 이 경우 TBAA는 float 가 별칭과 int를 별칭으로 지정할 수 없다는 것을 알고 i 의로드를 최적화합니다 .

이제 룰북으로

이 표준은 정확히 우리가 허용되고 허용되지 않는다고 무엇을 말합니까? 표준 언어는 간단하지 않으므로 각 항목마다 의미를 보여주는 코드 예제를 제공하려고합니다.

C11 표준은 무엇을 말합니까?

C11의 표준은 구역에 다음을 말한다 6.5 표현 제 7 항 :

객체는 다음 유형 중 하나를 갖는 lvalue 표현식으로 만 저장된 값에 액세스해야합니다. 88) — 객체의 유효 유형과 호환되는 유형,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

— 유효 객체 유형과 호환되는 유형의 정규화 된 버전

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

— 객체의 유효 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

GCC / 그 소리는 확장자가 할당 허용하는 부호없는 INT *을* INT 가 호환 유형이없는 경우에도 불구하고.

— 객체의 유효 유형의 정규화 된 버전에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

-구성원 중 상기 언급 된 유형 중 하나를 포함하는 집합 또는 조합 유형 (재귀 적으로 하위 집합 또는 포함 된 연합의 구성원 포함) 또는

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

— 문자 유형.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

C ++ 17 초안 표준의 내용

[basic.lval] 11 항의 C ++ 17 표준 초안 은 다음과 같습니다.

프로그램이 다음 유형 중 하나 이외의 glvalue를 통해 객체의 저장된 값에 액세스하려고하면 동작이 정의되지 않습니다. 63 (11.1) — 객체의 동적 유형,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) — 객체의 동적 유형의 cv-qualified 버전

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) — 객체의 동적 유형과 유사한 유형 (7.5에 정의 된대로)

(11.4) — 객체의 동적 유형에 해당하는 부호있는 유형 또는 부호없는 유형

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) — 객체의 동적 유형의 cv-qualified 버전에 해당하는 부호있는 또는 부호없는 유형 인 유형

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6)-요소 또는 비 정적 데이터 멤버 (재귀 적으로 하위 집합 또는 포함 된 유니온의 요소 또는 비 정적 데이터 멤버 포함) 중 위에서 언급 한 유형 중 하나를 포함하는 집계 또는 통합 유형

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) — 객체의 동적 유형에 대한 (cv cv-qualified) 기본 클래스 유형 인 유형

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) — 문자, 부호없는 문자 또는 std :: byte 유형.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

부호있는 char에 주목할 가치 가있는 것은 위의 목록에 포함되어 있지 않습니다 . 이것은 문자 유형 이라고 하는 C 와의 현저한 차이점입니다 .

타입 펀칭이란?

우리는이 시점에 도달했고 왜 우리가 별명을 밝히고 싶을 지 궁금 할 것입니다. 대답은 일반적으로 pun입력 하는 것이며, 종종 사용되는 방법이 엄격한 앨리어싱 규칙을 위반하는 것입니다.

때때로 우리는 타입 시스템을 우회하고 객체를 다른 타입으로 해석하려고합니다. 이것은 메모리 세그먼트를 다른 유형으로 재 해석하기 위해 유형 punning 이라고 합니다. 유형 제거 는 객체의 기본 표현에 액세스하여 보거나 전송하거나 조작하려는 작업에 유용합니다. 우리가 사용하는 타입 제거 (punning)는 컴파일러, 직렬화, 네트워킹 코드 등입니다.

전통적으로 이것은 객체의 주소를 가져 와서 재 해석하려는 유형의 포인터로 캐스팅 한 다음 값에 액세스하거나 다른 말로 별칭을 지정하여 수행되었습니다. 예를 들면 다음과 같습니다.

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

앞에서 보았 듯이 이것은 유효한 앨리어싱이 아니므로 정의되지 않은 동작을 호출합니다. 그러나 전통적으로 컴파일러는 엄격한 앨리어싱 규칙을 이용하지 않았으며 이러한 유형의 코드는 일반적으로 작동했습니다. 개발자는 불행히도 이런 식으로 일하는 데 익숙해졌습니다. 유형 정리의 일반적인 대체 방법은 공용체를 사용하는 것입니다. 공용체는 C에서는 유효하지만 C ++ 에서는 정의되지 않은 동작 입니다 ( 실례 참조 ).

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

이것은 C ++에서는 유효하지 않으며 일부는 노조의 목적이 변형 유형을 구현하기위한 것만 고려하고 유형 punning에 노조를 사용하는 것이 악용이라고 생각합니다.

우리는 어떻게 정확하게 Pun을 입력합니까?

C 및 C ++에서 유형 punning 의 표준 방법 은 memcpy 입니다. 이 손으로 조금 무거운를 보일 수 있지만, 최적화의 사용을 인식해야 방어 적이기 에 대한 형의 말장난을 하고 도망을 최적화하고 이동을 등록하는 레지스터를 생성합니다. 예를 들어 int64_tdouble 과 크기가 같다는 것을 알고 있다면 :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

우리는 memcpy 를 사용할 수 있습니다 :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

충분한 최적화 수준에서 현대의 모든 컴파일러는 이전에 언급 한 reinterpret_cast 메소드 또는 punning 유형의 공용체 메소드와 동일한 코드를 생성합니다 . 생성 된 코드를 살펴보면 register mov ( live Compiler Explorer Example ) 만 사용한다는 것을 알 수 있습니다.

C ++ 20 및 bit_cast

C ++ 20에서는 bit_cast ( proposal에서 링크로 사용 가능한 구현)를 얻을 수 있습니다.이를 통해 typepun을 간단하고 안전하게 수행하고 constexpr 컨텍스트에서 사용할 수 있습니다.

다음은 사용하는 방법에 대한 예입니다 bit_cast 말장난 입력 부호 INT플로트 ( 가 살고 참조 )

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

경우 에서 유형이 같은 크기를 가지고 있지 않습니다, 그것은 중간 struct15를 사용하는 우리를 필요로한다. 우리는 포함하는 구조체 사용합니다 를 sizeof (서명되지 않은 int)를 문자 배열 ( 가정 4 바이트 부호없는 INT를 로) 에서 유형 및 부호없는 INT 는 AS가 하기 입력 :

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

불행히도이 중간 유형이 필요하지만 이것이 bit_cast 의 현재 제약 조건입니다 .

엄격한 앨리어싱 위반 잡기

우리는 C ++에서 엄격한 앨리어싱을 잡기위한 좋은 도구가 많지 않습니다. 우리가 가지고있는 툴은 엄격한 앨리어싱 위반의 경우와 잘못 정렬 된로드 및 저장의 경우를 잡을 것입니다.

플래그 -fstrict-aliasing-Wstrict-aliasing을 사용하는 gcc는 오 탐지 / 음수가 아닌 경우도 일부 경우를 포착 할 수 있습니다. 예를 들어 다음과 같은 경우 gcc에서 경고가 생성됩니다 ( 실제 참조 ).

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

이 추가 사례를 포착하지는 않지만 ( 실제 참조 ) :

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

clang은 이러한 플래그를 허용하지만 실제로 경고를 구현하지는 않습니다.

우리가 사용할 수있는 또 다른 도구는 잘못 정렬 된로드와 저장을 잡을 수있는 ASan입니다. 이는 직접적인 앨리어싱 위반이 아니지만 엄격한 앨리어싱 위반의 일반적인 결과입니다. 예를 들어 -fsanitize = address를 사용하여 clang으로 빌드하면 다음과 같은 경우 런타임 오류가 발생합니다.

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

내가 추천 할 마지막 도구는 C ++ 전용이며 엄격하게 도구는 아니지만 코딩 방식이므로 C 스타일 캐스트를 허용하지 않습니다. gcc와 clang은 모두 -Wold-style- cast를 사용하여 C 스타일 캐스트에 대한 진단을 생성합니다 . 이렇게하면 정의되지 않은 유형의 펑이 reinterpret_cast를 사용하게되며, 일반적으로 reinterpret_cast는 자세한 코드 검토를위한 플래그 여야합니다. 감사를 수행하기 위해 reinterpret_cast에 대한 코드베이스를 검색하는 것이 더 쉽습니다.

C의 경우 이미 다룬 모든 도구가 있으며 C 언어의 큰 부분 집합에 대한 프로그램을 철저히 분석하는 정적 분석기 인 tis-interpreter도 있습니다. -fstrict-aliasing을 사용하는 경우에 한 가지 사례가 누락 된 이전 예제의 C 버전이 주어짐 ( 실제 참조 )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter는 세 가지를 모두 포착 할 수 있으며 다음 예제에서는 tis-kernal을 tis-interpreter로 호출합니다 (출력은 간결하게 편집 됨).

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

마지막으로 현재 개발중인 TySan 이 있습니다. 이 소독제는 섀도 메모리 세그먼트에 유형 검사 정보를 추가하고 앨리어싱 규칙을 위반하는지 확인합니다. 이 도구는 잠재적으로 모든 앨리어싱 위반을 포착 할 수 있어야하지만 런타임 오버 헤드가 클 수 있습니다.


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

3
내가 +10, 잘 작성하고 설명 할 수 있다면, 컴파일러 라이터와 프로그래머들로부터도 ... 유일한 비판 : 위의 카운터 예제를 가지고 표준에 의해 금지 된 것을 보는 것이 좋을 것입니다. 종류 :-)
Gabriel

2
아주 좋은 대답입니다. 초기 예제가 C ++로 제공되어 유감스럽게도 C를 알고 관심이 있고 무엇을 reinterpret_cast할 것인지 또는 무엇 cout을 의미 하는지 알지 못하는 나와 같은 사람들에게는 따르기가 어렵습니다 . (C ++를 언급하는 것은 괜찮지 만 원래 질문은 C와 IIUC에 관한 것이 었습니다.이 예제는 C로 올바르게 작성 될 수 있습니다.)
Gro-Tsen

유형 puning에 관해서 : 그래서 어떤 유형의 X 배열을 file에 쓰고, 그 파일에서 void *가 가리키는 메모리 로이 배열을 읽은 다음 그것을 사용하기 위해 포인터를 데이터의 실제 유형으로 캐스트합니다. 정의되지 않은 동작?
Michael IV

44

엄격한 앨리어싱은 포인터만을 가리키는 것이 아니며 참조에도 영향을 미칩니다. 부스트 개발자 위키에 대한 논문을 작성했으며 컨설팅 웹 사이트의 페이지로 전환하기에 충분히 받아 들였습니다. 그것은 그것이 무엇인지, 왜 사람들을 그렇게 혼란스럽게하는지, 그것에 대해 어떻게 해야하는지 완전히 설명합니다. 엄격한 앨리어싱 백서 . 특히 C ++에서 유니온이 위험한 행동 인 이유와 memcpy를 사용하는 것이 C와 C ++ 모두에서 유일한 이식 가능한 이유를 설명합니다. 이것이 도움이 되길 바랍니다.


3
" 엄격한 앨리어싱은 포인터만을 가리키는 것이 아니라 참조에도 영향을 미칩니다. "실제로는 lvalues를 나타냅니다 . " memcpy를 사용하는 것이 유일한 해결책입니다. "
curiousguy

5
좋은 종이. 필자의 견해 : (1)이 앨리어싱 문제는 나쁜 프로그래밍에 대한 과도한 반응이다. 나쁜 프로그래머를 나쁜 습관으로부터 보호하려는 것이다. 프로그래머가 좋은 습관을 가지고 있다면이 앨리어싱은 성가신 일이며 점검을 안전하게 끌 수 있습니다. (2) 컴파일러 측 최적화는 잘 알려진 경우에만 수행해야하며 확실하지 않은 경우 소스 코드를 엄격하게 준수해야합니다. 프로그래머가 컴파일러의 특이성을 충족시키기 위해 코드를 작성하도록 강요하는 것은 간단히 말해서 잘못입니다. 그것을 표준의 일부로 만드는 것은 더 나쁩니다.
slashmais

4
@slashmais (1) " 는 잘못된 프로그래밍에 대한 과잉 반응 입니다. 나쁜 습관을 거부하는 것입니다. 당신은 그렇게합니까? 당신은 가격을 지불 : 당신을 보장하지 않습니다! (2) 잘 알려진 경우? 어느 것? 엄격한 앨리어싱 규칙은 "잘 알려져"있어야합니다!
curiousguy

5
@curiousguy : 혼동의 몇 가지 요점을 정리 한 후, 앨리어싱 규칙을 사용하는 C 언어는 프로그램이 유형에 관계없이 메모리 풀을 구현할 수 없게한다는 것이 분명합니다. 일부 종류의 프로그램은 malloc / free로 얻을 수 있지만, 다른 종류의 프로그램은 현재 작업에 더 잘 맞는 메모리 관리 로직이 필요합니다. C89의 이론적 근거가 앨리어싱 규칙의 이유에 대한 그런 미묘한 예를 사용한 이유가 궁금합니다. 예를 들어 규칙이 합리적인 작업을 수행하는 데 큰 어려움을 겪지 않는 것처럼 보이기 때문입니다.
supercat

5
@ curiousguy, 대부분의 컴파일러 제품군에는 -O3에 기본으로 -fstrict-aliasing이 포함되어 있으며 TBAA에 대해 들어 본 적이 없으며 시스템 프로그래머와 같은 코드를 작성한 사용자에게는이 숨겨진 계약이 강제 적용됩니다. 필자는 시스템 프로그래머에게 불명료하게 들리는 것은 아니지만 이러한 종류의 최적화는 기본 옵션 인 -O3을 벗어나고 TBAA가 무엇인지 아는 사람들을위한 옵트 인 최적화 여야합니다. TBAA를 위반하는 사용자 코드, 특히 사용자 코드의 소스 레벨 위반을 추적하는 것으로 밝혀진 컴파일러 '버그'를 보는 것은 재미 있지 않습니다.
kchoi

34

Doug T.가 이미 쓴 것에 대한 부록으로서, 다음은 아마도 gcc로 트리거하는 간단한 테스트 사례입니다.

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

로 컴파일하십시오 gcc -O2 -o check check.c. 컴파일러는 "h"가 "check"함수에서 "k"와 같은 주소 일 수 없다고 가정하기 때문에 일반적으로 (대부분의 gcc 버전에서 시도한) "엄격한 앨리어싱 문제"를 출력합니다. 이 때문에 컴파일러는 if (*h == 5)자리 비움을 최적화 하고 항상 printf를 호출합니다.

여기에 관심이있는 사람들은 x64 용 우분투 12.04.2에서 실행되는 gcc 4.6.3에 의해 생성 된 x64 어셈블러 코드입니다.

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

따라서 if 조건은 어셈블러 코드에서 완전히 사라졌습니다.


check ()에 두 번째 짧은 * j를 추가하고 그것을 사용하면 (* j = 7) h와 j가 실제로 같은 값을 가리 키지 않으면 ggc가 최적화를 수행하지 않기 때문에 최적화 중단. 예, 최적화는 정말 똑똑합니다.
philippe lhardy

2
더 재미있게 만들려면 호환되지 않지만 크기와 표현이 같은 유형에 대한 포인터를 사용하십시오 (예 : long long*int64_t*에 해당하는 일부 시스템에서 ). 하나는 제정신 컴파일러는이 있음을 인식해야한다고 기대할 수 long long*int64_t*그들이 동일하게 저장하는 경우 동일한 스토리지에 액세스 할 수 있지만 이러한 치료는 더 이상 유행이다.
supercat

Grr ... x64는 Microsoft 규칙입니다. 대신 amd64 또는 x86_64를 사용하십시오.
SS Anne

Grr ... x64는 Microsoft 규칙입니다. 대신 amd64 또는 x86_64를 사용하십시오.
SS Anne

17

포인터 캐스트를 통한 타입 제거 (유니온 사용과 반대)는 엄격한 앨리어싱을 해제하는 주요 예입니다.


1
관련 인용문, 특히 각주에 대한 대답은 여기를 참조하십시오. 그러나 처음에는 제대로 표현되지 않았지만 C를 통해 유형 조합을 항상 뚫을 수 있습니다. 당신은 당신의 대답을 명확히하고 싶습니다.
Shafik Yaghmour

@ShafikYaghmour : C89는 구현 자들이 노조를 통해 유형 펑크를 인식하거나 유용하게 인식하지 못하는 경우를 명확하게 선택하도록 허용했습니다. 예를 들어, 프로그래머가 쓰기와 읽기 사이에 다음 중 하나를 수행 한 경우 구현은 한 유형에 대한 쓰기 다음에 다른 유형의 읽기가 유형 punning으로 인식되도록 지정할 수 있습니다 . 조합 유형 [시퀀스의 올바른 지점에서 수행되는 경우 구성원의 주소를 가져 가면 자격이 부여됨]; (2) 한 유형의 포인터를 다른 유형의 포인터로 변환하고 해당 ptr을 통해 액세스하십시오.
supercat

@ShafikYaghmour : 예를 들어 정수와 부동 소수점 값 사이의 유형 punning은 코드 fpsync()가 fp로 쓰기와 int로 읽기 또는 그 반대로 읽기 사이 의 지시문을 실행 한 경우에만 제대로 작동하도록 지정할 수 있습니다 ( 별도의 정수 및 FPU 파이프 라인 및 캐시가있는 구현에서) 그러한 지시어는 비싸지 만 컴파일러가 모든 유니언 액세스에서 그러한 동기화를 수행하는 것만 큼 비싸지는 않습니다. 또는 구현시 공통 초기 시퀀스를 사용하는 상황을 제외하고 결과 값을 사용할 수 없도록 지정할 수 있습니다.
supercat

@ShafikYaghmour : C89 하에서 구현 공용체를 포함하여 대부분의 형태의 punning을 금지 할 수 있지만, 공용체에 대한 포인터와 구성원에 대한 포인터 사이의 동등성은 명시 적으로 금지 하지 않는 구현에서는 형식 punning이 허용됨을 암시 했습니다.
supercat

17

C89의 이론적 근거에 따르면, 표준 작성자는 다음과 같은 코드를 컴파일러에 요구하지 않아도됩니다.

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

을 가리킬 x가능성을 허용하기 위해 대입 문과 return 문 사이 의 값을 다시로드해야 하고 대입 이 값을 변경할 수 있어야합니다 . 컴파일러가 위와 같은 상황에서 앨리어싱이 없을 것이라고 가정 할 수 있다는 개념은 논란의 여지가 없습니다.px*px

불행하게도, C89의 저자는 문자 그대로 읽을 경우 다음 함수조차도 정의되지 않은 동작을 호출하도록하는 방식으로 규칙을 작성했습니다.

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

그 형식의 좌변을 사용하므로 int액세스 타입의 오브젝트 struct Sint액세스 사용될 수있는 타입과는 다른 것이다 struct S. 문자가 아닌 유형의 구조체 및 공용체 멤버의 모든 사용을 정의되지 않은 동작으로 취급하는 것은 터무니없는 일이기 때문에, 거의 모든 사람들은 한 유형의 lvalue가 다른 유형의 객체에 액세스하는 데 사용될 수있는 상황이 적어도 있음을 인식합니다. . 불행히도 C 표준위원회는 그러한 상황을 정의하지 못했습니다.

대부분의 문제는 결함 보고서 # 028의 결과이며 다음과 같은 프로그램의 동작에 대해 질문했습니다.

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

결함 보고서 # 28은 "double"유형의 공용체 멤버를 작성하고 "int"유형 중 하나를 읽는 조치가 구현 정의 동작을 호출하기 때문에 프로그램이 정의되지 않은 동작을 호출한다고 설명합니다. 이러한 추론은 무의미하지만, 원래 문제를 해결하기 위해 아무 것도하지 않고 언어를 불필요하게 복잡하게하는 유효 유형 규칙의 기초를 형성합니다.

원래 문제를 해결하는 가장 좋은 방법은 규칙의 목적에 대한 각주를 규범적인 것처럼 취급하고 실제로 별칭을 사용하여 액세스가 충돌하는 경우를 제외하고 규칙을 시행 할 수 없게 만드는 것입니다. 다음과 같은 것이 주어진다 :

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

내 충돌도 없다 inc_int저장에 대한 모든 액세스를 통해 액세스하기 때문에 *p형식의 좌변과 완료는 int,와의 충돌이 없습니다 test때문에 p가시적에서 파생은 struct S, 다음 시간 s, 이제까지 될 것이다 저장에 대한 모든 액세스를 사용 를 통해이 p이미 일어난 것입니다.

코드가 약간 변경된 경우 ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

여기서, 실행중인 시점에 동일한 스토리지에 액세스하는 데 사용되는 다른 참조가 존재하기 때문에 표시된 행에 p대한 액세스와 s.x표시된 행에 대한 액세스 간에 앨리어싱 충돌이 발생 합니다 .

Defect Report 028은 원래 예제가 두 포인터의 생성과 사용 사이의 겹침으로 인해 UB를 호출했다고 말했는데, 이는 "유효 유형"또는 다른 복잡성을 추가하지 않고도 상황을 훨씬 더 명확하게 만들었습니다.


간단히 말해, 표준위원회가 할 수 있었던 것보다 훨씬 복잡하지 않은 목표를 달성 한 일종의 제안을 읽는 것이 흥미로울 것입니다.
jrh

1
@ jrh : 꽤 간단하다고 생각합니다. 1. 함수 또는 루프의 특정 실행 중에 앨리어싱이 발생하려면 충돌하는 fashon에서 동일한 스토리지를 처리하기 위해 실행 중에 두 개의 다른 포인터 또는 lvalue를 사용해야합니다 . 2. 하나의 포인터 또는 lvalue가 다른 포인터 또는 다른 값에서 새롭게 눈에 띄게 파생되는 상황에서, 두 번째에 대한 액세스는 첫 번째에 대한 액세스라는 것을 인식하십시오. 3. 실제로 앨리어싱을 포함하지 않는 경우에는 규칙이 적용되지 않음을 인식하십시오.
supercat

1
컴파일러가 새로 파생 된 lvalue를 인식하는 정확한 환경은 구현 품질 문제 일 수 있지만 원격으로 괜찮은 컴파일러는 gcc와 clang이 일부러 무시하는 형식을 인식 할 수 있어야합니다.
supercat

11

많은 답변을 읽은 후 무언가를 추가해야한다고 생각합니다.

다음과 같은 이유로 엄격한 앨리어싱 (나중에 설명하겠습니다) 이 중요합니다 .

  1. 메모리 액세스는 비용이 많이 들고 (성능면에서) 물리적 메모리에 다시 쓰기 전에 CPU 레지스터에서 데이터가 조작되는 이유 입니다.

  2. 서로 다른 두 CPU 레지스터의 데이터가 동일한 메모리 공간에 기록 되면 C로 코딩 할 때 어떤 데이터가 "생존"하는지 예측할 수 없습니다 .

    CPU 레지스터의로드 및 언로드를 수동으로 코딩하는 어셈블리에서는 어떤 데이터가 손상되지 않았는지 알 수 있습니다. 그러나 C는 (고맙게도)이 세부 사항을 추상화합니다.

두 포인터가 메모리에서 동일한 위치를 가리킬 수 있기 때문에 충돌 가능성을 처리하는 복잡한 코드 가 생길 수 있습니다 .

이 추가 코드는 느리고 필요하지 않은 추가 메모리 읽기 / 쓰기 작업을 수행하므로 성능 이 저하되고 성능 이 저하 됩니다.

엄격한 앨리어싱 규칙은 우리가 중복 기계 코드를 피할 수 는있는 경우에 해야한다 (또한 참조 두 개의 포인터가 같은 메모리 블록을 가리 키지 않는 것으로 가정하는 것이 안전 restrict키워드).

엄격한 앨리어싱은 다른 유형에 대한 포인터가 메모리의 다른 위치를 가리키는 것으로 가정하는 것이 안전하다고 말합니다.

컴파일러가 두 개의 포인터가 다른 유형 (예 : an int *및 a float *) 을 가리키는 것을 발견 하면 메모리 주소가 다르고 메모리 주소 충돌을 방지 하지 않으므로 기계 코드가 더 빨라집니다.

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

다음과 같은 기능을 가정 해 봅시다.

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

a == b(포인터가 동일한 메모리를 가리키는) 경우를 처리하려면 메모리에서 CPU 레지스터로 데이터를로드하는 방법을 주문하고 테스트해야하므로 코드는 다음과 같이 끝날 수 있습니다.

  1. 로드 a하고 b메모리에서.

  2. 추가 ab.

  3. 저장 b 하고 다시로드하십시오 a .

    (CPU 레지스터에서 메모리로 저장하고 메모리에서 CPU 레지스터로로드).

  4. 추가 ba.

  5. 저장 a메모리에합니다 (CPU 레지스터에서).

3 단계는 실제 메모리에 액세스해야하므로 매우 느립니다. 그러나, 인스턴스를 방지하기 위해 필요한 것 ab같은 메모리 주소를 가리 킵니다.

엄격한 앨리어싱을 사용하면 컴파일러에 이러한 메모리 주소가 명확하게 다르다는 사실을 알 수 있습니다 (이 경우 포인터가 메모리 주소를 공유하는 경우 수행 할 수없는 추가 최적화가 가능함).

  1. 서로 다른 유형을 사용하여 두 가지 방법으로 컴파일러에 알릴 수 있습니다. 즉 :

    void merge_two_numbers(int *a, long *b) {...}
  2. restrict키워드 사용 즉 :

    void merge_two_ints(int * restrict a, int * restrict b) {...}

이제 엄격한 앨리어싱 규칙을 충족하면 3 단계를 피할 수 있으며 코드가 훨씬 빠르게 실행됩니다.

실제로 restrict키워드 를 추가 하면 전체 기능을 다음과 같이 최적화 할 수 있습니다.

  1. 로드 a하고 b메모리에서.

  2. 추가 ab.

  3. a와에 결과를 모두 저장 합니다 b.

이 최적화 때문에 가능한 충돌의 이전에 완료되지 않았을 수있다 (여기서 ab배 대신에 두 배가 될 것이다).


3 단계에서 제한 키워드를 사용하여 결과를 'b'에만 저장하면 안됩니까? 합산 결과가 'a'에 저장된 것처럼 들립니다. 다시로드해야합니까?
NeilB

1
@NeilB-그렇습니다. 우리는 b(다시로드하지 않고) 저장 하고 다시로드 중 a입니다. 더 명확 해지기를 바랍니다.
Myst

유형 기반 앨리어싱은 이전에 몇 가지 이점을 제공했을 수도 restrict있지만 후자는 대부분의 환경에서보다 효과적이며 일부 제약 조건을 완화 register하면 restrict도움이되지 않는 경우를 채울 수 있다고 생각합니다 . 프로그래머가 컴파일러가 앨리어싱의 증거를 인식해야하는 모든 경우를 설명하는 것으로, 표준에 대한 특별한 증거가없는 경우에도 컴파일러가 앨리어싱을 추정해야하는 위치를 설명하는 것으로 간주하는 모든 경우를 설명하는 것으로 표준을 취급하는 것이 "중요한"지 확실하지 않습니다 .
supercat

메인 RAM에서의 로딩은 매우 느리지 만 (다음 작업이 결과에 의존하는 경우 CPU 코어를 오랫동안 정지시킬 수 있음), L1 캐시에서의 로딩은 매우 빠르므로 최근에 쓰는 캐시 라인에 쓰는 중입니다. 같은 핵심으로. 따라서 주소에 대한 첫 번째 읽기 또는 쓰기를 제외하고는 모두 합리적으로 빠릅니다. reg / mem addr 액세스 간의 차이는 캐시 된 / catched mem addr 간의 차이보다 작습니다.
curiousguy

@curiousguy-비록 정확하지만이 경우 "빠른"은 상대적입니다. L1 캐시는 아마도 CPU 레지스터보다 10 배 이상 느릴 것입니다 (10 배 이상 느리다고 생각합니다). 또한 restrict키워드는 작업 속도뿐만 아니라 그 수도 최소화합니다. 이는 의미가있을 수 있습니다. 결국 가장 빠른 작업은 전혀 작업하지 않습니다. :)
Myst

6

엄격한 앨리어싱은 동일한 데이터에 대해 다른 포인터 유형을 허용하지 않습니다.

이 기사 는 문제를 자세하게 이해하는 데 도움이됩니다.


4
참조와 참조 및 포인터 사이의 별칭을 지정할 수도 있습니다. 내 자습서 참조 dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

4
동일한 데이터에 대해 다른 포인터 유형을 가질 수 있습니다. 엄격한 앨리어싱이 발생하는 곳은 동일한 메모리 위치가 한 포인터 유형을 통해 쓰여지고 다른 포인터 유형을 통해 읽는 경우입니다. 또한 일부 다른 유형이 허용됩니다 (예 : int및을 포함하는 구조체 int).
MM

-3

기술적으로 C ++에서 엄격한 앨리어싱 규칙은 적용 할 수 없습니다.

간접 정의 ( * operator ) 의 정의에 유의하십시오 .

단항 * 연산자는 간접을 수행합니다. 적용되는 표현식은 객체 유형에 대한 포인터 또는 함수 유형에 대한 포인터 여야하며 결과는 표현식이 가리키는 객체 또는 함수 참조하는 lvalue 입니다.

또한 glvalue의 정의에서

glvalue는 평가가 객체의 신원을 결정하는 표현식입니다 (... 조각).

따라서 잘 정의 된 프로그램 추적에서 glvalue는 객체를 나타냅니다. 따라서 소위 엄격한 앨리어싱 규칙은 적용되지 않습니다. 이것은 디자이너가 원하는 것이 아닐 수도 있습니다.


4
C 표준은 "개체"라는 용어를 사용하여 여러 가지 다른 개념을 나타냅니다. 그중에서 어떤 목적으로 독점적으로 할당되는 바이트 시퀀스, 특정 유형의 값을 쓰거나 읽을 수있는 바이트 시퀀스에 대한 불필요한 배타적 참조 또는 실제로 가지고 있는 참조 일부 상황에서 액세스되거나 액세스됩니다. 나는 표준이 사용하는 모든 방식과 일치하는 "객체"라는 용어를 정의하는 현명한 방법이 없다고 생각합니다.
supercat

@supercat 잘못되었습니다. 당신의 상상력에도 불구하고, 그것은 실제로 상당히 일관됩니다. ISO C에서는 "실행 환경의 데이터 저장 영역 (내용은 값을 나타낼 수 있음)"으로 정의됩니다. ISO C ++에서도 비슷한 정의가 있습니다. 언급 한 것은 객체의 내용 을 참조 하는 표현 방법이기 때문에 귀하의 의견은 답변과 관련이 없습니다. 답변 은 객체 의 정체성 과 밀접한 관련이있는 표현의 C ++ 개념 (glvalue)을 보여줍니다 . 그리고 모든 앨리어싱 규칙은 기본적으로 내용과는 관련이 있지만 신원과 관련이 있습니다.
FrankHB

1
@ FrankHB : 선언 int foo;하면 lvalue 식으로 무엇에 액세스 *(char*)&foo합니까? 그 유형의 개체 char입니까? 그 물체는 동시에 존재 foo합니까? foo위에서 언급 한 유형의 객체의 저장된 값 을 변경하기 위해 글을 쓰시겠습니까 char? 그렇다면 유형 char의 lvalue를 사용하여 유형의 객체의 저장된 값에 액세스 할 수있는 규칙이 int있습니까?
supercat

@FrankHB : 6.5p7이 없으면 모든 스토리지 영역에 동시에 해당 스토리지 영역에 맞는 모든 유형의 모든 객체가 포함되며 해당 스토리지 영역에 액세스하면 동시에 모든 객체에 액세스 할 수 있다고 말할 수 있습니다. 그러나 6.5p7에서 "객체"라는 용어를 이러한 방식으로 해석하면 문자형이 아닌 lvalue로 많은 작업을 수행 할 수 없으며, 이는 명백한 결과가 아니며 규칙의 목적을 완전히 상실하게됩니다. 또한 6.5p6 이외의 다른 곳에서 사용되는 "개체"라는 개념에는 정적 컴파일 타임 유형이 있지만 ...
supercat

1
sizeof (int)는 4이며, 선언 int i;은 각 문자 유형 in addition to one of type int ? I see no way to apply a consistent definition of "object" which would allow for operations on both * (char *) & i` 및의 4 개의 객체를 만듭니다 i. 마지막으로, 표준 volatile에는 "객체"의 정의를 충족하지 않는 하드웨어 레지스터에 액세스 할 수있는 정규화 된 포인터 조차 없습니다.
supercat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.