엄격한 에일리어스 규칙이 뭐죠?
C의 일반적인 정의되지 않은 동작에 대해 질문할 때 엄밀한 에일리어스 규칙을 참조하는 경우가 있습니다.
슨슨 얘기 ?? ???
엄밀한 등)를 하는 입니다.uint32_t
""uint16_t
에 구조물을 오버레이 을 통해 이러한 엄밀한 .
따라서 이런 종류의 설정에서는 메시지를 보내려면 동일한 메모리 청크를 가리키는 두 개의 호환되지 않는 포인터가 있어야 합니다.그러면 이렇게 순진하게 코드화할 수 있습니다.
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
모든 루프를 통과합니다.대해 되지 않은 몇 가지 할 때 할 수 .「 」 「 」 「 」 「 」 「 」 「 」 「 」 「 」 「 」 「 」 「 」 「 」 。buff[0]
★★★★★★★★★★★★★★★★★」buff[1]
루프가 실행되기 전에 CPU 레지스터에 접속하여 루프 본체의 속도를 높입니다.는 의 .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);
}
컴파일러는 Send Message를 인라인화할 수 있는지 없는지 또는 충분히 스마트하지 않을 수 있으며, 다시 버퍼를 로드할지 여부를 결정할 수 있습니다. ifSendMessage
별도로 컴파일된 다른 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에 액세스할 수 있는 유형은 다음과 같습니다.
- 개체의 유효 유형과 호환되는 유형,
- 개체의 유효 유형과 호환되는 유형의 정규 버전,
- 오브젝트의 유효 타입에 대응하는 부호 있는 타입 또는 부호 없는 타입,
- 오브젝트의 유효한 타입의 정규 버전에 대응하는 부호 있는 타입 또는 부호 없는 타입의 타입.
- 조합원 중 앞에서 언급한 유형 중 하나를 포함하는 집합체 또는 조합 유형(소집합체 또는 포함된 조합의 구성원을 포함),
- 문자형
메모
이것은 제가 쓴 글에서 발췌한 것입니다. "엄격한 에일리어싱 규칙이란 무엇이며 왜 우리는 신경을 쓰는가?"
엄밀한 에일리어스가 뭐죠?
C 및 C++ 에일리어스는 저장된 값에 액세스할 수 있는 식 유형과 관련이 있습니다.C와 C++에서는 모두 표준이 어떤 식 타입을 에일리어스 할 수 있는지를 지정합니다.컴파일러와 옵티마이저는 에일리어스 규칙을 엄격하게 따르는 것으로 간주할 수 있습니다.따라서 엄밀한 에일리어스 규칙이라는 용어를 사용합니다.not allowed 유형을 사용하여 값에 액세스하려고 하면 정의되지 않은 동작(UB)으로 분류됩니다.동작을 정의하지 않으면 모든 내기가 무효가 됩니다.프로그램의 결과는 더 이상 신뢰할 수 없습니다.
유감스럽게도 엄밀한 에일리어싱 위반으로 인해 예상한 결과를 얻을 수 있으며, 새로운 최적화가 적용된 컴파일러의 향후 버전은 유효하다고 생각되는 코드를 파괴할 가능성이 있습니다.이는 바람직하지 않으며 엄격한 에일리어스 규칙을 이해하고 이를 위반하지 않는 방법을 이해하는 것이 가치 있는 목표입니다.
이 문제에 대해 자세히 이해하기 위해 엄밀한 에일리어스 규칙을 위반했을 때 발생하는 문제, 타입 펀닝에 사용되는 일반적인 기법이 엄밀한 에일리어스 규칙을 위반하는 경우가 많기 때문에 타입 펀닝, 펀을 올바르게 입력하는 방법에 대해 설명합니다.
예비 예
몇 가지 예를 살펴본 후 표준이 정확히 무엇을 말하는지 설명하고, 몇 가지 예를 더 살펴본 다음, 엄밀한 에일리어싱을 피하고 놓친 위반을 포착하는 방법을 알아봅니다.다음은 놀라운 예가 될 수 있습니다(실시간 예).
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
int*는 int에 의해 점유된 메모리를 가리키며 이는 유효한 에일리어스입니다.옵티마이저는 ip를 통한 할당이 x가 점유하는 값을 갱신할 수 있다고 가정해야 합니다.
다음 예시는 정의되지 않은 동작을 일으키는 에일리어스를 나타내고 있습니다(라이브 예).
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를 포함하는 동일한 메모리 위치를 가리키도록 두 파라미터를 설정합니다.refret_cast는 템플릿파라미터에 의해 지정된 타입이 있는 것처럼 표현식을 취급하도록 컴파일러에 지시하고 있습니다.이 경우, 표현 &x를 float* 타입으로 취급하도록 지시하고 있습니다.두 번째 cout의 결과는 순진하게 0이라고 예상할 수 있지만 -O2를 사용하여 최적화를 활성화하면 gcc와 clang 모두 다음과 같은 결과를 얻을 수 있습니다.
0
1
어느지만 완벽하게 우리가 정의되지 않음을 행동으로 호출된 유효한 기대가 되지 않을지도 모른다.꽃 수레는 정당하게 한int 개체 alias 수 없다.따라서 optimizer부터 f를 통해 가게는 정당하게 한int 개체에게 영향을 미칠 수 없을 때가 될 것 반환 값 역 참조 1상수 저장된 추정할 수 있다.컴파일러에 사용자가 코드 연결 및 해제 이것이 정확히 무슨 happening( 살아 있는 예)어 있다.을 보여 준다.
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
그 optimizerType-Based 앨리어스 분석(TBAA)을 사용하여 1과 직접 반환 값을 레지스터 eax에 상수 값을 움직여 반환될 것으로 가정한다.TBAA 유형 특별하기 짐과 상점을 최적화할 수 있는지에 대한 언어 규칙을 사용한다.이 경우 TBAA은 변동 환율제와 intalias 수는 없으며 i.의 하중을 최적화하고
이제, Rule-Book
은 정확히 어떤 표준 할 수 허용된 한다고 말하는가?표준 언어, 그렇게 각 항목에 대해 나는 그 뜻을 보여 줍니다 코드 예제를 제공하기 위해서 노력할 것입니다 간단하지 않다.
그 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/clang음에도 그들이 호환되는 형식 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 수식 버전
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 수식 버전에 대응하는 부호 있는 유형 또는 부호 없는 유형입니다.
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-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) : char, unsigned char 또는 std::바이트 타입.
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
}
부호 있는 문자는 위의 목록에 포함되지 않습니다.이것은 문자 타입을 나타내는 C와 현저한 차이입니다.
타입 퍼닝이란
이제 이 지경에 이르렀는데, 왜 우리가 에일리어스를 붙이려고 하는지 궁금할 수도 있습니다.일반적으로 pun을 입력하면 됩니다.사용되는 메서드는 엄밀한 에일리어스 규칙을 위반하는 경우가 많습니다.
때로는 타입 시스템을 우회하여 오브젝트를 다른 타입으로 해석하고 싶을 때도 있습니다.기억의 한 부분을 다른 유형으로 재해석하는 것을 타입 펀닝이라고 합니다.유형 펀닝은 개체의 기본 표현에 액세스하여 표시, 전송 또는 조작하려는 작업에 유용합니다.유형 펀닝이 사용되는 대표적인 분야는 컴파일러, 시리얼화, 네트워크 코드 등입니다.
기존에는 오브젝트의 주소를 가져와 해석하고 싶은 타입의 포인터에 붙여 그 값에 액세스 하거나 다른 말로 에일리어싱을 함으로써 실현되어 왔습니다.예를 들어 다음과 같습니다.
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++에서는 유효하지 않으며, 일부에서는 조합의 목적이 변형 유형을 구현하기 위한 것이며 유형 펀닝에 조합을 사용하는 것은 남용이라고 생각한다.
펀을 올바르게 입력하려면 어떻게 해야 하나요?
C와 C++ 모두에서 타입 펀닝의 표준 방법은 memcpy입니다.이것은 다소 어려운 것처럼 보일 수 있지만, 최적화 도구는 memcpy를 유형 펀닝에 사용하는 것을 인식하고 이를 최적화하여 레지스터를 생성하여 이동을 등록해야 합니다.예를 들어 int64_t가 double과 같은 크기라고 알고 있는 경우:
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);
//...
충분한 최적화 레벨에서, 모든 괜찮은 현대 컴파일러는 앞서 언급한 react_cast 메서드 또는 타입 펀닝을 위한 유니언 메서드와 동일한 코드를 생성한다.생성된 코드를 조사하면 register mov(라이브 컴파일러 탐색기 예)만 사용됩니다.
C++20 및 비트캐스트
C++20에서는 constexpr 컨텍스트에서 사용할 수 있을 뿐만 아니라 type-pun에 대한 간단하고 안전한 방법을 제공하는 bit_cast(프로포절 링크에서 사용 가능)를 얻을 수 있습니다.
다음으로 bit_cast를 사용하여 fun signed int를 입력하여 부동하는 예를 나타냅니다(실시간 참조).
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
To 타입과 From 타입의 사이즈가 동일하지 않은 경우는 중간 구조를 사용해야 합니다.Size of ( unsigned int )문자 배열(4바이트 unsigned int로 가정)을 포함하는 구조를 From 유형으로 사용하고 To 유형으로 unsigned int를 사용합니다.
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는 false positive/negative가 없는 경우는 아니지만 경우에 따라서는 검출할 수 있습니다.예를 들어 다음과 같은 경우 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-style cast에 대한 진단을 생성합니다.이렇게 하면 정의되지 않은 유형의 펀이 refret_cast를 사용하도록 강제됩니다.일반적으로 refret_cast는 보다 상세한 코드 리뷰를 위한 플래그가 됩니다.또한 코드 베이스에서 react_cast를 검색하여 감사를 수행하는 것도 간단합니다.
C에는 이미 모든 툴이 준비되어 있습니다.또한 C언어의 대규모 서브셋에 대해 프로그램을 철저히 분석하는 스태틱아나라이저인 tis-interpreter도 준비되어 있습니다.-fstrict-aliasing을 사용하면 1개의 케이스가 누락되는 앞의 예에 대한 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-controlter는 이 세 가지를 모두 잡을 수 있습니다.다음 예에서는 tis-control을 tis-controlter로 호출합니다(출력은 간결하게 편집됩니다).
./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이 있습니다.이 검사 기능은 섀도 메모리 세그먼트에 유형 검사 정보를 추가하고 액세스를 검사하여 앨리어스 규칙을 위반하는지 확인합니다.툴은 에일리어스 위반을 모두 검출할 수 있지만 런타임 오버헤드가 클 수 있습니다.
내가 찾은 최고의 설명은 Mike Acton, Understrict Aliasing의 설명입니다.PS3 개발에 중점을 두고 있지만 기본적으로는 GCC에 불과합니다.
기사 내용:
엄밀한 에일리어스는 C(또는 C++) 컴파일러에 의해 만들어진 가정입니다.다른 타입의 오브젝트에 대한 참조 포인터는 같은 메모리 위치(즉, 서로 에일리어스)를 참조하지 않습니다.)"
으로는 ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★int*
기억을 가리키다.int
에 포인트 하는 거예요.float*
float
.만약 당신의 코드가 이것을 존중하지 않는다면 컴파일러의 옵티마이저가 당신의 코드를 망가뜨릴 것입니다.
는 「」입니다.char*
모든 유형을 가리킬 수 있습니다.
이것은 C++03 표준의 섹션 3.10에 기재되어 있는 엄밀한 에일리어스 규칙입니다(다른 답변은 적절한 설명을 제공하지만 규칙 자체는 제공하지 않습니다).
프로그램이 다음 유형 중 하나 이외의 값을 사용하여 객체의 저장된 값에 액세스하려고 하면 동작은 정의되지 않습니다.
- 객체의 동적 유형,
- 객체의 동적 유형의 cv 한정 버전,
- 객체의 동적 유형에 대응하는 부호 있는 유형 또는 부호 없는 유형입니다.
- 객체의 동적 유형의 cv 수식 버전에 대응하는 부호 있는 유형 또는 부호 없는 유형.
- 조합원 중 앞에서 언급한 유형 중 하나를 포함하는 집합체 또는 조합 유형(소집합체 또는 포함된 조합의 구성원을 포함)
- 객체의 동적 유형의 기본 클래스 유형(cv-qualified)인 유형,
- a
char
★★★★★★★★★★★★★★★★★」unsigned char
discloss.discloss.
C++11 및 C++14 표현(변화 강조):
프로그램이 다음 유형 중 하나 이외의 glvalue를 통해 객체의 저장된 값에 액세스하려고 하면 동작은 정의되지 않습니다.
- 객체의 동적 유형,
- 객체의 동적 유형의 cv 한정 버전,
- 객체의 동적 유형과 유사한 유형(4.4에서 정의)
- 객체의 동적 유형에 대응하는 부호 있는 유형 또는 부호 없는 유형입니다.
- 객체의 동적 유형의 cv 수식 버전에 대응하는 부호 있는 유형 또는 부호 없는 유형.
- 해당 요소 또는 비정적 데이터 멤버(소집계 또는 포함 결합의 요소 또는 비정적 데이터 멤버 포함) 중 앞서 언급한 유형 중 하나를 포함하는 집합 또는 결합 유형
- 객체의 동적 유형의 기본 클래스 유형(cv-qualified)인 유형,
- a
char
★★★★★★★★★★★★★★★★★」unsigned char
discloss.discloss.
lvalue가 아닌 glvalue와 Aggregate/union 케이스의 명확화라는 두 가지 변경은 미미했습니다.
세 번째 변경은 보다 강력한 보증을 제공합니다(강력한 에일리어스 규칙을 완화합니다).에일리어스에 안전한 새로운 개념의 유사 유형.
또한 C 문구(C99; ISO/IEC 9899:1999 6.5/7, ISO/IEC 9899:2011 §6.5 7에서 정확히 동일한 문구가 사용됨):
오브젝트는 다음 중 하나의 유형의 lvalue 식을 통해서만 저장된 값에 액세스할 수 있어야 합니다.
- 개체의 유효 유형과 호환되는 유형,
- 개체의 유효 유형과 호환되는 유형의 한정 버전
- 오브젝트의 유효 타입에 대응하는 부호 있는 타입 또는 부호 없는 타입,
- 오브젝트의 유효 타입의 한정 버전에 대응하는 부호 있는 타입 또는 부호 없는 타입,
- 조합원 중 앞에서 언급한 유형 중 하나를 포함하는 집합체 또는 조합 유형(소집합체 또는 포함된 조합의 구성원을 포함),
- 문자형
73) or 88) 이 목록의 목적은 객체가 에일리어스 될 수도 있고 그렇지 않을 수도 있습니다.
C89의 논거에 따르면, 이 기준서의 작성자들은 작성자들이 다음과 같은 코드를 부여받기를 원하지 않았다.
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
「」의 , 이.x
와 return 에서는, 「」가 될 이 있습니다.p
수 x
에의 *p
으로 과적으 might might 、 might might 、 。x
컴파일러가 위와 같은 상황에서 에일리어스가 없다고 가정할 권리가 있다는 개념은 논란의 여지가 없었다.
불행히도 C89의 저자들은 문자 그대로 읽으면 다음 함수도 정의되지 않은 동작을 호출하도록 규칙을 작성했습니다.
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
lvalue 의 입니다.int
의 struct S
, , , , 입니다.int
는, 액세스 할 때 할 수 타입이 아닙니다.struct S
구조나 조합의 문자 타입이 아닌 멤버의 모든 사용을 정의되지 않은 동작으로 취급하는 것은 불합리하기 때문에, 거의 모든 사람들은 적어도 한 유형의 l값이 다른 유형의 객체에 접근하기 위해 사용될 수 있는 상황이 있다는 것을 인식하고 있습니다.불행히도 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
in , ,는 in in in 。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를 호출했다고 말했다면, "유효한 유형"이나 다른 복잡성을 추가하지 않고도 훨씬 더 명확하게 할 수 있었을 것입니다.
많은 답을 읽은 후, 다음과 같이 덧붙일 필요가 있습니다.
엄밀한 에일리어싱(잠시 후에 설명하겠습니다)은 다음과 같은 이유로 중요합니다.
메모리 액세스는 비용이 많이 들 수 있습니다(퍼포먼스에 유의).이 때문에 데이터가 물리 메모리에 다시 쓰기 전에 CPU 레지스터에서 조작됩니다.
두 개의 다른 CPU 레지스터에 있는 데이터가 동일한 메모리 공간에 기록될 경우, C로 코딩할 때 어떤 데이터가 "존재"할지는 예측할 수 없습니다.
조립에서는 CPU 레지스터의 로딩과 언로딩을 수동으로 코드화하여 어떤 데이터가 손상되지 않았는지 알 수 있습니다.그러나 C는 (감사하게도) 이 세부사항을 추상화한다.
2개의 포인터가 메모리내의 같은 장소를 가리킬 수 있기 때문에, 이로 인해, 생각할 수 있는 콜리젼을 처리하는 복잡한 코드가 발생할 가능성이 있습니다.
이 추가 코드는 속도가 느리고 메모리 읽기/쓰기 작업이 느리고 불필요하기 때문에 성능이 저하됩니다.
엄밀한 에일리어스 규칙을 사용하면 2개의 포인터가 같은 메모리블록을 가리키지 않는다고 가정해도 안전할 경우 다중 머신코드를 회피할 수 있습니다(「」도 참조).restrict
키워드를 지정합니다.
엄밀한 에일리어싱에서는, 다른 타입의 포인터가 메모리내의 다른 장소를 가리킨다고 하는 것이 안전합니다.
을 가리키고 있는 를 들어, 2개의 포인터가 다른 타입을 있는 경우),int *
a. a. a.float *
메모리 주소가 다르다고 가정하고, 메모리 주소의 충돌로부터 보호되지 않기 때문에 머신 코드가 고속화됩니다.
예를 들어 다음과 같습니다.
다음과 같은 기능을 가정합니다.
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
「 」의에 , 「 」a == b
모두 같은 레지스터로 는 다음과 같이 「 」 「 」 「 」 「 」 「 」 「 」 「 」 「 」 「 」 「 CPU 」 「 」을 사용하다
a
★★★★★★★★★★★★★★★★★」b
기억에서.a
로로 합니다.b
.b
및 새로고침a
.(CPU 레지스터에서 메모리로 저장하고 메모리에서 CPU 레지스터로 로드합니다).
b
로로 합니다.a
.a
(CPU 레지 ( ( ( ( ( ( ( ( ( ( ( ( ( ()
스텝 3은 물리 메모리에 액세스 할 필요가 있기 때문에 매우 느립니다.단, 다음과 같은 경우로부터 보호해야 합니다.a
★★★★★★★★★★★★★★★★★」b
같은 메모리 주소를 가리킵니다.
엄밀한 에일리어스를 사용하면 컴파일러에 이들 메모리주소가 확연히 다르다는 것을 알려줌으로써 이를 방지할 수 있습니다(이 경우 포인터가 메모리주소를 공유하면 실행할 수 없는 추가 최적화가 가능하게 됩니다).
이는 컴파일러에게 두 가지 방법으로 나타낼 수 있습니다.즉, 다음과 같습니다.
void merge_two_numbers(int *a, long *b) {...}
「 」의
restrict
★★★★★★★★★★★★★★★★:void merge_two_ints(int * restrict a, int * restrict b) {...}
이제 Strict Aliasing 규칙을 충족함으로써 스텝3을 회피할 수 있고 코드 실행이 대폭 빨라집니다.
'아까운'을 restrict
키워드, 기능 전체를 다음과 같이 최적화할 수 있습니다.
a
★★★★★★★★★★★★★★★★★」b
기억에서.a
로로 합니다.b
.를 양쪽에
a
★★★★★★★★★★★★★ ★★★★b
.
는 이전까지는.충돌 입니다(여기서 '충돌'은 '충돌'을 일으킬 수 있습니다.a
★★★★★★★★★★★★★★★★★」b
두 배가 아니라 세 배가 될 것이다.)
엄밀한 에일리어싱에서는 같은 데이터에 다른 포인터 타입을 사용할 수 없습니다.
이 문서는 문제를 자세히 이해하는 데 도움이 될 것입니다.
엄밀한 에일리어스는 포인터뿐만 아니라 레퍼런스에도 영향을 미칩니다.저는 Boost 개발자 Wiki를 위해 그것에 대한 논문을 썼는데 매우 반응이 좋아서 컨설팅 웹사이트의 페이지로 만들었습니다.그것이 무엇인지, 왜 그렇게 많은 사람들을 혼란스럽게 하는지, 그리고 그것에 대해 무엇을 해야 하는지 완전히 설명합니다.엄밀한 에일리어싱 화이트 페이퍼특히 조합이 C++에게 위험한 동작인 이유와 memcpy를 사용하는 것이 C와 C++ 모두에서 이식 가능한 유일한 수정 방법인 이유를 설명합니다.이게 도움이 됐으면 좋겠네요.
포인터 캐스트를 통한 타입 펀닝(유니온 사용과는 반대)은 엄밀한 에일리어싱을 해제하는 주요 예입니다.
Doug T.가 이미 작성한 내용의 부록으로, gcc를 사용하여 트리거할 수 있는 간단한 테스트 사례를 소개합니다.
체크.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)
인쇄하다
여기에 관심이 있는 분은 x64용 ubuntu 12.04.2에서 실행되는 gcc 4.6.3에 의해 생성된 x64 어셈블러 코드를 참조해 주십시오.
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
어셈블러 코드에서 if 조건이 완전히 사라졌습니다.
C++에서는 엄밀한 에일리어스 규칙은 적용되지 않을 수 있습니다.
indirection(* 연산자)의 정의에 주의해 주세요.
unary * 연산자는 indirection을 수행합니다.이 연산자가 적용되는 식은 객체유형에 대한 포인터 또는 함수유형에 대한 포인터여야 합니다.결과는 식이 가리키는 객체 또는 함수를 참조하는 l값입니다.
glvalue는 개체의 ID를 결정하는 식입니다(...snip).
잘 정의된 프로그램 추적에서 glvalue는 객체를 나타냅니다.그래서 소위 말하는 엄격한 에일리어스 규칙은 절대 적용되지 않습니다.이것은 디자이너가 원했던 것이 아닐 수도 있습니다.
언급URL : https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule
'programing' 카테고리의 다른 글
Vuex 스토어의 돌연변이 중 하나에서 커밋을 호출할 수 있습니까? (0) | 2022.07.20 |
---|---|
유틸리티 클래스 설정 및 vue 구성 요소 내에서 사용 (0) | 2022.07.20 |
VueJ: 여러 컴포넌트의 Vuex getter가 작동하지 않는 것을 감시한다. (0) | 2022.07.20 |
스프링 경유 RESTful 인증 (0) | 2022.07.20 |
OpenGL에서 glOrtho()를 사용하는 방법 (0) | 2022.07.20 |