2008-10-21 4 views
11

오픈 소스 program I wrote에서는 파일에서 다른 프로그램으로 작성된 이진 데이터를 읽고 ints, double, 및 기타 여러 가지 데이터 유형을 출력합니다. 문제 중 하나는 이 양쪽 endiannesses의 32 비트 및 64 비트 시스템에서 실행되어야한다는 것입니다. 즉, 은 저수준 비트 twiddling을 꽤 많이 수행해야합니다. 나는 (매우) 약간의 punning과 엄격한 앨리어싱에 대해서 알고 있으며, 내가 올바르게 일을하는지 확인하고 싶다.안전하게 COOL에서 char *를 두들겨 쓰는 방법 *

기본적으로, 다양한 크기의 int로 숯불 * 변환을 쉽게 :

int64_t snativeint64_t(const char *buf) 
{ 
    /* Interpret the first 8 bytes of buf as a 64-bit int */ 
    return *(int64_t *) buf; 
} 

하고 필요에 따라 나는 그런 로, 바이트 순서를 교환하는 지원 기능의 캐스팅이 있습니다

런타임시
int64_t swappedint64_t(const int64_t wrongend) 
{ 
    /* Change the endianness of a 64-bit integer */ 
    return (((wrongend & 0xff00000000000000LL) >> 56) | 
      ((wrongend & 0x00ff000000000000LL) >> 40) | 
      ((wrongend & 0x0000ff0000000000LL) >> 24) | 
      ((wrongend & 0x000000ff00000000LL) >> 8) | 
      ((wrongend & 0x00000000ff000000LL) << 8) | 
      ((wrongend & 0x0000000000ff0000LL) << 24) | 
      ((wrongend & 0x000000000000ff00LL) << 40) | 
      ((wrongend & 0x00000000000000ffLL) << 56)); 
} 

, 프로그램은 시스템의 엔디안 니스를 검출하고 함수 포인터 위의 을 대입

012,308,505,353,

이제는 char *를 이중으로 캐스팅하려고 할 때 까다로운 부분이 있습니다.

union 
{ 
    double d; 
    int64_t i; 
} int64todouble; 

int64todouble.i = slittleint64_t(bufoffset); 
printf("%lf", int64todouble.d); 

그러나 일부 컴파일러는 "int64todouble.i"할당 을 멀리 최적화 프로그램을 깰 수 : 나는 지금처럼 엔디안 스와핑 코드를 다시 사용하고 싶습니다. 이 프로그램이 성능을 위해 최적화되어 있어야하며, 은 double * 에 char *를 캐스팅하기 위해 병렬 변환 집합을 쓰지 않는 것을 선호한다고 생각하면 안전한 방법이 있습니까? 노조 방법이 안전하다면, snatchint64_t와 같은 함수를 다시 써야합니까?

snativeint64_t: 
     movq (%rdi), %rax 
     ret 
:

int64_t snativeint64_t(const char *buf) 
{ 
    /* Interpret the first 8 bytes of buf as a 64-bit int */ 
    int64_t output; 
    memcpy(&output, buf, 8); 
    return output; 
} 

내 원래의 코드와 똑같은 어셈블러로 컴파일 :


나는 변환 기능을 다시 작성하기 때문에, 방어 적이기를 사용과 같이 할 Steve Jessop's 대답을 사용하여 종료

두 버전 중 memcpy 버전은 내가하려는 일을보다 명확하게 표현하고 가장 순진한 컴파일러에서도 작동해야합니다.

아담, 당신의 대답도 훌륭했고 나는 그것에 대해 많은 것을 배웠습니다. 게시 해 주셔서 감사합니다!

+0

정수 변환은 char 포인터가 충분히 잘 정렬되어있는 경우에만 안전합니다. –

+0

이 프로그램에서, 그것은 항상있을 것입니다. –

+0

런타임에서 엔디안을 전혀 감지하지 못하는 이유는 무엇입니까? 나는이 프로그램이 특정 endian-ness를 가질 컴파일 된 아치에서만 작동 할 수 있다고 상상할 것입니다. 그래서 여러분은 ntohl을 좋아하고 컴파일 시간 결정을 내리지 않습니까? –

답변

2

int64_t와 double이 같은 크기이고 적합한 저장 표현을 가지고 있는지 구현에 대해 충분히 알고있는 것처럼 보이기 때문에 memcpy가 위험 할 수 있습니다. 그렇다면 앨리어싱에 대해서 생각할 필요조차 없습니다.

여러 바이너리를 릴리스하려는 경우 쉽게 인라인 될 수있는 함수 포인터를 사용하고 있으므로 성능은 큰 문제가 아니어야하지만 일부 컴파일러는 상당히 복잡 할 수 있음을 알고 싶을 수도 있습니다 fiendish 최적화 memcpy - 작은 정수 크기의 경우로드 및 저장 세트가 인라인 될 수 있으며 변수가 완전히 최적화되어 컴파일러가 "복사"를 수행하여 단순히 변수에 사용하는 스택 슬롯을 재 할당하면됩니다. 노조처럼.

int64_t i = slittleint64_t(buffoffset); 
double d; 
memcpy(&d,&i,8); /* might emit no code if you're lucky */ 
printf("%lf", d); 

결과 코드를 검사하거나 프로파일 링하십시오. 최악의 경우라도 속도가 느릴 가능성은 없습니다.

그러나 일반적으로 바이트 와이핑으로 너무 영리 해지면 이식성 문제가 발생합니다. 중간 엔디 언 더블 즈를 가진 ABI가 있습니다. 각 단어는 리틀 엔디안이지만 큰 단어가 먼저옵니다.

일반적으로 sprintf 및 sscanf를 사용하여 복식 저장을 고려할 수 있지만 프로젝트의 경우 파일 형식은 사용자가 제어 할 수 없습니다. 하지만 응용 프로그램이 한 형식의 입력 파일에서 다른 형식의 출력 파일로 IEEE double을 삽킹하는 경우 (해당되는 경우 데이터베이스 형식을 알 수 없으므로 확신 할 수는 없지만 그렇다면) 어쨌든 산술에 사용하지 않기 때문에 두 배라는 사실을 잊을 수 있습니다. 파일 형식이 다른 경우에만 바이트 짝핑을 요구하는 불투명 한 char [8]로 취급하십시오.

+0

위대한 memcpy 팁 - 감사합니다! 실제로 텍스트 형식으로 출력을 두배로해야합니다 또는 난 그냥 원시 바이트 주위에 슬링 거라고. 또한 함수 포인터가있는 경우와없는 함수를 많이 사용하여 프로파일 링했습니다 (큰 영향을받는 경우 엔 큰 엔디안을 건너 뛸 수 있었기 때문에).하지만 측정 가능한 차이는 없었습니다. –

12

매우 좋습니다. Understanding Strict Aliasing을 읽어보십시오. 특히, "조합을 통한 캐스팅"섹션을 참조하십시오. 아주 좋은 예가 많이 있습니다. 이 기사는 셀 프로세서에 관한 웹 사이트에 있고 PPC 어셈블리 예제를 사용하고 있지만 거의 모든 것이 x86을 포함한 다른 아키텍처에도 동일하게 적용될 수있다.

+0

감사합니다. 그게 제가 찾고 있던 일입니다. 나는 지금 읽을 것이다. –

+0

@ryan_s : 고마워요, 고정 –

2

표준에 따르면 유니온의 한 필드에 쓰고 즉시 읽는 것은 정의되지 않은 동작입니다. 따라서 규칙 책을 읽으면 노동 조합에 기반한 방법이 효과가 없을 것입니다.

매크로는 일반적으로 좋지 않지만 규칙에 예외가 될 수 있습니다.입력 및 출력 유형을 매개 변수로 사용하는 매크로 세트를 사용하여 C에서 템플리트와 유사한 동작을 얻을 수 있어야합니다.

+0

GCC 매뉴얼은 "-fstrict-aliasing이라 할지라도, 유형 펀치가 허용됩니다. 단, 메모리는 공용체 유형을 통해 접근해야합니다." 그걸 충분히 호기 롭고 유혹하지만 컴파일러 관련 코드를 작성하는 것은 싫다. 매크로 예제에 대한 포인터가 있습니까? –

0

매우 작은 하위 제안으로서 64 비트 경우에서 마스킹 및 전환을 바꿀 수 있는지 조사해 보는 것이 좋습니다. 작업이 바이트를 바꿔 쓰고 있기 때문에 항상 0xff의 마스크로 벗어날 수 있어야합니다. 이것은 컴파일러가 그 자체를 파악할만큼 똑똑하지 않다면 더 빠르고 더 컴팩트 한 코드로 이어질 것입니다. 이 변화 간단히

:

(((wrongend & 0xff00000000000000LL) >> 56) 

이것으로 :

((wrongend >> 56) & 0xff) 

동일한 결과를 생성한다.

+0

다른 모든 것들은 비트를 출력의 중간으로 이동시키기 때문에 첫 번째 마스크 -와 - 쉬프트 연산에서만 작동합니다. –

+0

사실, 마스킹 후 다시 이동해야합니다. 거대한 상수 (나에게)를 피하는 것이 좋기 때문에 아마 그렇게하는 것이 더 좋을 것이다. 바이트를 추출 할 때 byte-for-byte를 사용하면 더 잘 작동합니다. – unwind

-1

편집 : 대한
제거 된 의견이 얼마나 효과적으로 질문자가 언급되지 않은 것처럼 다른 프로그램이 자신의 데이터를 (중요한 정보 인) 기록, 데이터 항상 큰 엔디 기계 엔디안에 교환을 저장합니다.

데이터가 엔디안에서 빅 및 엔디안에서 빅 엔테이션으로 변환해야하는 경우 ntohs/ntohl/htons/htonl이 가장 우수하고 속도면에서 가장 우아하고 탁월합니다 (CPU가 지원하는 경우 하드웨어에서 작업을 수행하므로 , 당신은 그것을 이길 수 없다). 더블/플로트에 대해서는


, 단지 메모리 주조로의 int로 저장 :

int64_t doubleToInt64(double d) 
{ 
    return *(int64_t *)&d; 
} 

double int64ToDouble(int64_t i) 
{ 
    return *(double *)&i; 
} 

질문자는이 링크를 제공

double d = 3.1234; 
printf("Double %f\n", d); 
int64_t i = *(int64_t *)&d; 
// Now i contains the double value as int 
double d2 = *(double *)&i; 
printf("Double2 %f\n", d2); 

함수로 랩 :

http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html

캐스팅이 좋지 않다는 증명으로 ... 불행히도이 페이지의 대부분은 강력하게 동의 할 수 없습니다. 지수 및 의견 : 는 포인터를 통해 캐스팅으로, 실제로 나쁜 연습과 잠재적으로 위험한 코드가

같은 일반적인. 포인터를 통해 을 캐스팅하면 에 잠재적 인 가능성이 있기 때문에 형식이 틀리기 때문에 버그가 생성됩니다.

전혀 위험하지 않으며 나쁜 습관도 아닙니다. 잘못 작성하면 버그가 발생할 가능성이 있습니다. C 언어의 프로그래밍이 잘못 수행하면 버그가 발생할 가능성이 있으므로 모든 언어의 프로그래밍도 마찬가지입니다. 그 논쟁에 의해 당신은 프로그래밍을 완전히 중단해야합니다.

유형 말장난
포인터 에일리어싱의 형태로 두 개의 포인터와 메모리 같은 위치 참조하지만 해당 위치 상이한 유형을 나타낸다. 컴파일러는 "puns"를 관련없는 포인터로 취급합니다.유형 punning은 두 데이터 모두 에 액세스 할 수있는 잠재적 인 영향을 미칩니다.

이것은 사실이지만 불행히도 은 내 코드과 전혀 관련이 없습니다. 그는가 참조 무엇

은 다음과 같이 코드 :

int64_t * intPointer; 
: 
// Init intPointer somehow 
: 
double * doublePointer = (double *)intPointer; 

이제 doublePointer 및 intPointer 같은 메모리 위치에 두 점,하지만 같은 유형으로이 치료. 이것은 당신이 정말로 노동 조합으로 해결해야하는 상황이며, 다른 것은 상당히 나쁘다. 나쁜 것은 내 코드가하는 것과 다릅니다!

내 코드 복사 에 의해, 참조에 의해. 나는 int64 포인터 (또는 다른 방법으로 둥근)에 double을 캐스팅하고 은 즉시을 복종시킨다. 함수가 반환되면 포인터는 아무 것도 보유하지 않습니다. int64와 double이 있으며 함수의 입력 매개 변수와 완전히 관련이 없습니다. 나는 다른 타입의 포인터에 어떤 포인터도 복사하지 않는다. (코드 샘플에서 이것을 보았을 때 나는 쓴 C 코드를 잘못 읽었다.) 나는 그 값을 다른 메모리 타입의 변수에 전달한다. . 따라서 "메모리에서 같은 위치를 참조하십시오"라는 말처럼 타입 펀칭의 정의는 전혀 적용되지 않으며 여기서는 같은 메모리 위치를 의미하지 않습니다.

int64_t intValue = 12345; 
double doubleValue = int64ToDouble(intValue); 
// The statement below will not change the value of doubleValue! 
// Both are not pointing to the same memory location, both have their 
// own storage space on stack and are totally unreleated. 
intValue = 5678; 

내 코드는 단지 외부 기능없이 C로만 작성된 메모리 복사본입니다.

int64_t doubleToInt64(double d) 
{ 
    return *(int64_t *)&d; 
} 

그것은 그 이상 아무것도

int64_t doubleToInt64(double d) 
{ 
    int64_t result; 
    memcpy(&result, &d, sizeof(d)); 
    return result; 
} 

로 작성, 그래서 어디서나 눈에 심지어 말장난 어떤 종류가 없습니다 될 수 있습니다. 그리고이 연산은 연산이 C로 할 수있는 것처럼 완전히 안전합니다. double은 항상 64 비트로 정의됩니다 (int와는 달리 크기가 변하지 않으며 64 비트로 고정되어 있음). 따라서 항상 적합합니다 int64_t 크기의 변수로 변환합니다.

+0

첫 번째 단계에서 프로그램은 다른 프로그램에서 생성 한 데이터를 읽습니다. 두 번째 요점은 이것에 frowned 것으로 보인다 : http://cocoawithlove.com/2008/04/using-pointers-to-recast-in-c-is-bad.html 그리고 제가 부탁하는 부분은 내가 그 일에서 완전히 벗어나야하는지. –

+0

위 업데이트를 참조하십시오. 연결된 페이지 클레임에는 아무런 타입도 없습니다. 그리고 당신의 코드와는 달리 나는 결코 절대적으로 안전하지 않은 것처럼 char 포인터를 절대 절대 캐스팅하지 않는다. 모든 데이터를 값으로 전달한다. (절대 참조하지 않는다!) 그리고 같은 크기를 보장하는 타입들 사이에서만 캐스팅한다. – Mecki

+0

값을 전달하는 것은 성능상의 이유로 코드에서 불가능합니다. 나는 결코 char 포인터를 던지지 않는다; 나는 그 내용을 던졌습니다. 마지막으로 ntoh *는 빅 엔디안 값을 캐스팅하는 경우에만 작동합니다. 리틀 엔디안 값에는 해당 기능이 없습니다. –

관련 문제