programing

컴파일러에 의한 구조 재정렬

minimums 2023. 7. 22. 09:52
반응형

컴파일러에 의한 구조 재정렬

다음과 같은 구조가 있다고 가정합니다.

struct MyStruct
{
  uint8_t var0;
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};

이것은 아마도 많은 공간을 낭비할 것입니다.은 이은필정때다니문입렬요한것▁▁of의 필요한 정렬 때문입니다.uint32_t변수.

할 수 한 후)uint32_t변수) 다음과 같이 보일 수 있습니다.

struct MyStruct
{
  uint8_t var0;
  uint8_t unused[3];  //3 bytes of wasted space
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};

보다 효율적인 구조는 다음과 같습니다.

struct MyStruct
{
  uint8_t var0;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
  uint32_t var1;
};

이제 질문은 다음과 같습니다.

컴파일러가 (표준에 의해) 구조를 다시 정렬하는 것이 금지된 이유는 무엇입니까?

구조물을 다시 주문하면 발에 총을 쏠 수 있는 방법이 없을 것 같습니다.

컴파일러가 (표준에 의해) 구조를 다시 정렬하는 것이 금지된 이유는 무엇입니까?

기본적인 이유는 C와의 호환성 때문입니다.

C는 원래 고급 어셈블리 언어입니다. C로 하여 메모리 패킷, ...)를 보는 이 매우 입니다.struct.

이로 인해 이 속성에 의존하는 여러 기능이 있습니다.

  • 는 C의 했습니다.struct첫입니다.virtual상속/상속)

  • C는 당신이 두 개를 가지고 있다면 보장합니다.struct A그리고.B 다 멤버 둘 다 데 터시로니합으로 합니다.char 멤버 멤다표에다니시됩음버데이가 나옵니다.int(그리고 그 후 무엇이든), 그러면 당신이 그것들을 넣을 때.union을 쓸 수 .B회원이 되어 책을 읽습니다.char그리고.int 그을통여하것을 통하여.AC++도 마찬가지입니다. 표준 레이아웃입니다.

후자는 매우 광범위하며 대부분의 경우 데이터 멤버의 재주문을 완전히 방지합니다.struct(또는)class).


C에는 액세스 제어 개념이 없으므로 C++은 액세스 제어 지정자가 다른 두 데이터 멤버의 상대 순서를 지정하지 않도록 지정합니다.

제가 아는 한, 어떤 컴파일러도 그것을 이용하려고 시도하지 않지만, 이론적으로는 그럴 수 있습니다.

C++ 외부에서는 Rust와 같은 언어를 사용하여 컴파일러가 필드를 다시 정렬할 수 있으며 기본적으로 메인 Rust 컴파일러(rustc)가 이를 수행합니다.역사적인 결정과 하위 호환성에 대한 강한 열망만이 C++가 그렇게 하는 것을 방해합니다.

구조물을 다시 정렬하면 발에 총을 쏠 수 있는 방법이 없을 것 같습니다.

정말요? 만약 이것이 허용된다면, 동일한 프로세스에서도 라이브러리/모듈 간의 통신은 기본적으로 터무니없이 위험할 것입니다.

"In Universe" 주장

우리는 우리의 구조가 우리가 요청한 방식으로 정의되어 있다는 것을 알 수 있어야 합니다.패딩이 지정되지 않은 것은 충분히 나쁜 일입니다!다행히도 필요할 때 제어할 수 있습니다.

자, 이론적으로, 새로운 언어가 만들어질 수 있습니다. 마찬가지로, 구성원들은 어떤 속성이 주어지지 않는 한 순서를 바꿀 수 있습니다.결국, 우리는 객체에 대해 메모리 레벨 마법을 수행하지 않기 때문에 C++ 관용구만 사용한다면 기본적으로 안전할 것입니다.

하지만 그것은 우리가 살고 있는 현실이 아닙니다.


"우주 밖" 주장

당신의 말대로 "매번 동일한 주문이 사용되었다"면 안전할 수 있습니다.그 언어는 회원들이 어떻게 주문을 받을 것인지 분명하게 명시해야 할 것입니다.그것은 표준으로 쓰기에는 복잡하고, 이해하기에는 복잡하고, 구현하기에는 복잡합니다.

명령이 코드에 있는 그대로 될 것임을 보장하고 이러한 결정을 프로그래머에게 맡기는 것이 훨씬 쉽습니다.기억하세요, 이 규칙들은 오래된 C에서 비롯되었고 오래된 C는 프로그래머에게 힘을 줍니다.

사소한 코드 변경으로 구조체를 패딩 효율적으로 만드는 것이 얼마나 쉬운지 이미 질문에서 보여주었습니다.이를 위해 언어 수준에서 복잡성을 추가할 필요가 없습니다.

이 표준은 구조체가 데이터 프로토콜이나 하드웨어 레지스터 모음과 같은 특정 메모리 레이아웃을 나타낼 수 있기 때문에 할당 순서를 보장합니다.예를 들어, 프로그래머나 컴파일러 모두 TPC/IP 프로토콜의 바이트 순서나 마이크로컨트롤러의 하드웨어 레지스터를 다시 정렬할 수 없습니다.

만약 주문이 보장되지 않았다면,structsC++ 벡터와 유사한 단순하고 추상적인 데이터 컨테이너일 것이며, 그 안에 어떤 식으로든 우리가 넣은 데이터를 포함하고 있다는 점을 제외하고는 많은 것을 가정할 수 없습니다.그것은 낮은 수준의 프로그래밍을 할 때 그들을 실질적으로 더 쓸모없게 만들 것입니다.

컴파일러는 구조가 다른 컴파일러 또는 다른 언어에 의해 생성된 다른 하위 수준 코드에 의해 읽힐 경우를 대비하여 구성원의 순서를 유지해야 합니다.당신이 운영체제를 만들고 있는데, 당신이 그것의 일부를 C로 쓰기로 결정하고, 그것의 일부를 어셈블리로 쓰기로 결정했다고 치자.다음 구조를 정의할 수 있습니다.

struct keyboard_input
{
    uint8_t modifiers;
    uint32_t scancode;
}

구조물의 메모리 레이아웃을 수동으로 지정해야 하는 어셈블리 루틴으로 이를 전달합니다.4바이트 정렬이 있는 시스템에서 다음 코드를 작성할 수 있습니다.

; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]

컴파일러가 구현 정의된 방식으로 구조체의 멤버 순서를 변경한다고 가정하면, 이는 사용하는 컴파일러와 전달하는 플래그에 따라 스캔 코드 멤버의 첫 번째 바이트 또는 수정자 멤버로 끝날 수 있음을 의미합니다.

물론 문제는 단순히 어셈블리 루틴을 사용하는 낮은 수준의 인터페이스로 축소될 뿐만 아니라 다른 컴파일러로 빌드된 라이브러리가 서로를 호출하는 경우에도 발생합니다(예: Windows API를 사용하여 mingw로 프로그램을 빌드).

이것 때문에, 언어는 여러분이 구조 레이아웃에 대해 생각하도록 강요합니다.

패킹을 개선하기 위해 요소를 자동으로 다시 정렬하면 특정 메모리 레이아웃이나 이진 직렬화에 영향을 줄 수 있을 뿐만 아니라, 속성 순서는 프로그래머가 자주 사용되는 멤버의 캐시 로컬성을 더 적게 액세스하는 멤버에 비해 이점을 주기 위해 신중하게 선택했을 수 있습니다.

Dennis Ritchie가 설계한 언어는 구조의 의미를 행동의 측면이 아니라 메모리 레이아웃의 측면에서 정의했습니다.구조물 S가 오프셋 X에서 유형 T의 멤버 M을 갖는 경우, M의 거동입니다.S는 S의 주소를 가져와서 X바이트를 추가하고, T에 대한 포인터로 해석하고, 그에 따라 식별된 스토리지를 l 값으로 해석하는 것으로 정의되었습니다.구조 구성원을 작성하면 연결된 저장소의 내용이 변경되고 구성원의 저장소 내용이 변경되면 구성원의 값이 변경됩니다.코드는 구조 구성원과 관련된 스토리지를 조작하는 다양한 방법을 자유롭게 사용할 수 있었으며, 의미론은 해당 스토리지의 작업 측면에서 정의되었습니다.

코드가 구조와 관련된 저장소를 조작할 수 있는 유용한 방법 중 하나는 memcpy()를 사용하여 한 구조의 임의 부분을 다른 구조의 해당 부분에 복사하거나 memset()을 사용하여 구조의 임의 부분을 지우는 것이었습니다.구조 구성원이 순차적으로 배치되었기 때문에 단일 memcpy() 또는 memset() 호출을 사용하여 구성원 범위를 복사하거나 지울 수 있습니다.

표준 위원회에서 정의한 언어는 대부분의 경우 구조 구성원에 대한 변경사항이 기본 스토리지에 영향을 미치거나 스토리지에 대한 변경사항이 구성원 값에 영향을 미쳐 구조 레이아웃에 대한 보장이 Ritchie의 언어에 비해 유용하지 않게 됩니다.그럼에도 불구하고 memcpy()와 memset()을 사용하는 기능은 유지되었고, 구조 요소를 순차적으로 유지해야 하는 기능을 유지했습니다.

당신은 또한 C++의 말을 인용하고 있는데, 왜 그런 일이 일어날 수 없는지에 대한 실질적인 이유를 말씀드리겠습니다.

사이에 차이가 없다는 점을 고려하여 다음 사항을 고려하십시오.

class MyClass
{
    string s;
    anotherObject b;

    MyClass() : s{"hello"}, b{s} 
    {}

};

이제 C++에서는 정적이 아닌 데이터 멤버를 선언된 순서대로 초기화해야 합니다.

그런 다음 정적이 아닌 데이터 멤버는 클래스 정의에 선언된 순서대로 초기화됩니다.

[]base.class.init/13에 따라따라서 컴파일러는 클래스 정의 내의 필드를 재정렬할 수 없습니다. 다른 구성원의 초기화에 따라 (예를 들어) 다른 구성원이 작동할 수 없기 때문입니다.

컴파일러가 메모리에서 다시 정렬하지 않아도 된다는 것은 엄격하게 요구되지 않지만, 특히 위의 예를 고려할 때, 그것을 추적하는 것은 매우 고통스러울 것입니다.그리고 저는 패딩과 달리 성능이 향상될지 의심스럽습니다.

이더넷 패킷과 같이 이 구조 레이아웃이 실제로 '와이어를 통해' 수신된 메모리 시퀀스라고 상상해 보십시오. 컴파일러가 더 효율적으로 작업을 다시 정렬한다면, 올바른 순서와 위치에 모든 올바른 바이트를 가진 구조를 사용하는 것이 아니라 필요한 순서로 바이트를 추출하는 작업을 많이 해야 할 것입니다.

언급URL : https://stackoverflow.com/questions/38244689/struct-reordering-by-compiler

반응형