[c] 비트 단위 연산으로 예기치 않은 가변 크기

문맥

원래 PIC 마이크로 컨트롤러 용 8 비트 C 컴파일러를 사용하여 컴파일 된 C 코드를 포팅하고 있습니다. 부호없는 전역 변수 (예 : 오류 카운터)가 0으로 롤오버되는 것을 방지하기 위해 사용 된 일반적인 관용구는 다음과 같습니다.

if(~counter) counter++;

여기서 비트 연산자는 모든 비트를 반전 시키며 명령문은 counter최대 값보다 작은 경우에만 참 입니다. 중요한 것은 변수 크기에 관계없이 작동합니다.

문제

현재 GCC를 사용하여 32 비트 ARM 프로세서를 대상으로하고 있습니다. 우리는 동일한 코드가 다른 결과를 생성한다는 것을 알았습니다. 우리가 알 수있는 한, 비트 보수 연산은 우리가 예상했던 것과 다른 크기의 값을 반환하는 것처럼 보입니다. 이것을 재현하기 위해 GCC에서 컴파일합니다.

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

출력의 첫 번째 줄에서 우리 i는 1 바이트입니다. 그러나 비트 단위의 보수 i는 실제로 4 바이트 이므로이 문제와 비교하면 예상 한 결과를 얻지 못하므로 문제가 발생합니다. 예를 들어, i제대로 초기화 된 uint8_t경우 :

if(~i) i++;

i0xFF에서 다시 0x00으로 “랩핑” 이 표시됩니다 . 이 동작은 이전 컴파일러와 8 비트 PIC 마이크로 컨트롤러에서 의도했던대로 작동 할 때와 비교하여 GCC에서 다릅니다.

다음과 같이 캐스팅하여이 문제를 해결할 수 있습니다.

if((uint8_t)~i) i++;

또는

if(i < 0xFF) i++;

그러나이 해결 방법 모두에서 변수의 크기를 알고 있어야하며 소프트웨어 개발자에게 오류가 발생하기 쉽습니다. 이러한 종류의 상한 검사는 코드베이스 전체에서 발생합니다. 이 변수의 여러 크기는 (예., uint16_tunsigned char등)와 달리 작업 코드베이스에서 이러한 변화는 우리가 기대하고있는 것이 아닙니다.

질문

문제에 대한 이해가 올 바르며이 관용구를 사용한 각 사례를 다시 방문하지 않아도되는 문제를 해결하기위한 옵션이 있습니까? 비트 단위 보수와 같은 연산이 피연산자와 동일한 크기의 결과를 반환해야한다는 가정이 맞습니까? 프로세서 아키텍처에 따라 이것이 깨질 것 같습니다. 나는 미친 약을 복용하고 있다고 느끼고 C는 이것보다 조금 이식성이 있어야합니다. 다시 말하지만, 이것에 대한 우리의 이해는 틀릴 수 있습니다.

표면적으로 이것은 큰 문제처럼 보이지 않을 수도 있지만 이전에 작동했던이 관용구는 수백 곳에서 사용되며 값 비싼 변경을 진행하기 전에이를 이해하기를 간절히 바라고 있습니다.


참고 : 여기에는 겉으로는 비슷하지만 정확한 중복 질문이 있습니다 : char의 비트 단위 연산은 32 비트 결과를 제공합니다

나는 거기에서 논의 된 문제의 실제 요점을 보지 못했습니다. 즉 비트 보완의 결과 크기가 연산자에 전달 된 것과 다릅니다.



답변

당신이보고있는 것은 정수 프로모션 의 결과입니다 . 식에 정수 값이 사용되는 대부분의 경우 값의 유형이 값보다 작 으면 int값이로 승격됩니다 int. 이것은 C 표준의 6.3.1.1p2 섹션에 설명되어 있습니다 .

다음은 표현에 사용될 수있다 목적지 int또는
unsigned int사용될 수있다

  • 정수 변환 순위가 및 의 순위보다 작거나 같은 정수 유형 ( int또는 이외의 unsigned int) 을 갖는 객체 또는 표현식입니다 .intunsigned int
  • 유형의 비트 필드 _Bool, int ,INT 서명 , or서명되지 않은 int`합니다.

int비트 필드에 대해 너비에 의해 제한되는대로 원래 유형의 모든 값을 나타낼 수있는 경우 값은 int; 그렇지 않으면으로 변환됩니다
unsigned int. 이를 정수 승격 이라고합니다 . 다른 모든 유형은 정수 승격에 의해 변경되지 않습니다.

따라서 변수에 유형 uint8_t과 값이 255 인 경우 캐스트 또는 할당 이외의 연산자를 사용 int하면 조작을 수행하기 전에 먼저 값이 255 인 유형 으로 변환 됩니다. 이것이 sizeof(~i)1 대신 4를 제공하는 이유 입니다.

6.5.3.3 절에서는 정수 승격이 ~연산자에 적용된다고 설명합니다 .

~연산자 의 결과는 (승격 된) 피연산자 의 비트 단위 보수 입니다 (즉, 변환 된 피연산자의 해당 비트가 설정되지 않은 경우에만 결과의 각 비트가 설정 됨). 정수 승격은 피연산자에서 수행되며 결과에는 승격 된 유형이 있습니다. 승격 된 유형이 부호없는 유형 인 경우 표현식 ~E은 해당 유형에서 표현할 수있는 최대 값 빼기와 같습니다 E.

그래서 32 비트를 가정 할 int경우, counter8 비트 값이 0xff는 32 비트 값으로 변환됩니다 0x000000ff및 적용 ~당신을 제공에 0xffffff00.

아마도 이것을 처리하는 가장 간단한 방법은 유형을 알 필요없이 증분 후 값이 0인지 확인하는 것입니다.

if (!++counter) counter--;

부호없는 정수의 줄 바꿈은 양방향으로 작동하므로 값을 0으로 줄이면 가장 큰 양수 값을 얻습니다.


답변

에서 는 sizeof (I); 변수 i 의 크기를 요청 하므로 1

에서 (~ i)를 sizeof 연산자; 귀하의 경우에는 표현식 유형의 크기를 int 인 크기로 요청합니다.


쓰다

if (~ i)

내가 255를 (값이 uint8_t 인 경우) 값을 읽지 못하는지 알기 위해 그냥 읽으십시오.

if (i != 255)

휴대용 코드를 읽을 수 있습니다


여러 크기의 변수가 있습니다 (예 : uint16_t 및 unsigned char 등).

부호없는 크기를 관리하려면 다음을 수행하십시오.

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

식은 일정하므로 컴파일 타임에 계산됩니다.

#INCLUDE <limits.h> 대한 CHAR_BIT사용법 #include <stdint.h> 대한 uintmax_t


답변

다음은 부호없는 정수 유형 인 x경우 ” 1을 추가 하지만 최대 표현 가능한 값으로 고정 “을 구현하는 몇 가지 옵션입니다 x.

  1. x유형에 표시 가능한 최대 값보다 작은 경우에만 하나를 추가하십시오 .

    x += x < Maximum(x);

    의 정의에 대해서는 다음 항목을 참조하십시오 Maximum. 이 방법은 비교, 조건부 집합 또는 이동의 형태, 추가와 같은 효율적인 명령어에 대해 컴파일러에 의해 최적화 될 가능성이 높습니다.

  2. 유형의 가장 큰 값과 비교하십시오.

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (이것은 2 계산 N , N은 비트의 개수는 x, 2 내지 시프트로 N -1 비트. 우리 대신 1 변속이 수행 N 타입의 비트 수만큼 시프트는 C에 의해 정의되지 않았기 때문에 비트 표준. CHAR_BIT매크로 일부 생소 할 수도 그렇게, 바이트의 비트의 개수 sizeof x * CHAR_BIT의 입력 비트의 수이다 x.)

    미학과 선명도를 위해 매크로에 매크로로 감쌀 수 있습니다.

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. 다음을 x사용하여 0으로 줄 바꿈하면 증가 하고 수정하십시오 if.

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. x표현식을 사용하여 0으로 줄 바꿈하면 증가 하고 정정하십시오.

    ++x; x -= !x;

    이것은 명목상 분기가 없으며 (때로는 성능에 유리함) 컴파일러는 필요한 경우 분기를 사용하여 위와 동일하게 구현할 수 있지만 대상 아키텍처에 적합한 명령어가있는 경우 무조건 명령어로 가능합니다.

  5. 위의 매크로를 사용하는 분기없는 옵션은 다음과 같습니다.

    x += 1 - x/Maximum(x);

    x유형이 최대 값 인 경우 로 평가됩니다 x += 1-1. 그렇지 않으면입니다 x += 1-0. 그러나 많은 아키텍처에서는 분할이 다소 느립니다. 컴파일러는 컴파일러와 대상 아키텍처에 따라 나누지 않고 명령어에 맞게이를 최적화 할 수 있습니다.


답변

stdint.h 이전에는 변수 크기가 컴파일러마다 다를 수 있으며 C의 실제 변수 유형은 여전히 ​​int, long 등이며 컴파일러 작성자는 여전히 크기에 대해 정의합니다. 일부 표준이나 대상별 가정이 아닙니다. 그런 다음 저자는 두 세계를 맵핑하기 위해 stdint.h를 작성해야합니다. 즉, uint_this를 int, long, short로 맵핑하는 stdint.h의 목적입니다.

다른 컴파일러에서 코드를 포팅하고 char, short, int, long을 사용하는 경우 각 유형을 거치고 포트를 직접 수행해야합니다. 그리고 변수에 맞는 크기로 선언하면 선언이 변경되지만 작성된 코드는 작동합니다 ….

if(~counter) counter++;

또는 … 마스크 나 타입 캐스트를 직접 제공

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

하루가 끝나면이 코드가 작동하려면 새 플랫폼으로 이식해야합니다. 방법에 대한 당신의 선택. 그렇습니다. 각 사례에 시간을 투자하여 올바르게 수행해야합니다. 그렇지 않으면이 코드로 계속 돌아와서 훨씬 더 비쌉니다.

이식하기 전에 코드에서 변수 유형을 분리하고 변수 유형의 크기를 결정한 경우,이를 수행하는 변수를 분리하고 (그 리핑하기 쉬워야 함) 향후 변경되지 않을 stdint.h 정의를 사용하여 선언을 변경하십시오. 그리고 당신은 놀랐지 만 잘못된 헤더가 때때로 사용되기 때문에 체크를 넣어 밤에 더 잘 수 있습니다.

if(sizeof(uint_8)!=1) return(FAIL);

그리고 그 스타일의 코딩 작업 (if (~ counter) counter ++;), 현재와 미래의 이식성을 위해 마스크를 사용하여 크기를 구체적으로 제한하고 선언에 의존하지 않는 것이 가장 좋습니다. 코드는 처음에 작성되거나 포트를 완성한 다음 다른 요일에 다시 포트 할 필요가 없습니다. 또는 코드를 더 읽기 쉽게 만들려면 if x <0xFF then 또는 x! = 0xFF 또는 그와 비슷한 것을 수행하십시오. 컴파일러는 이러한 솔루션 중 하나와 동일한 코드로 코드를 최적화하여 더 읽기 쉽고 덜 위험하게 만듭니다. …

제품이 얼마나 중요한지 또는 몇 번이나 패치 / 업데이트를 보내거나 트럭을 굴 리거나 실험실로 걸어 가서 빠른 솔루션을 찾거나 영향을받는 코드 라인을 터치하는지 여부에 따라 문제를 해결하려는 경우에 따라 다릅니다. 백 개나 적은 수라면 그다지 큰 포트가 아닙니다.


답변

6.5.3.3 단항 산술 연산자

4 연산자 의 결과 ~는 (승격 된) 피연산자의 비트 단위 보수입니다. 즉, 변환 된 피연산자의 해당 비트가 설정되지 않은 경우에만 결과의 각 비트가 설정됩니다. ). 정수 승격은 피연산자에서 수행되며 결과에는 승격 된 유형이 있습니다. 승격 된 유형이 부호없는 유형 인 경우 표현식 ~E은 해당 유형에서 표현할 수있는 최대 값 빼기와 같습니다 E.

C 2011 온라인 초안

문제는 연산자가 적용되기 전에 피연산자 ~가 승격되고 있다는 것 int입니다.

불행히도, 나는 이것에서 쉬운 방법이 없다고 생각합니다. 쓰기

if ( counter + 1 ) counter++;

프로모션도 적용되므로 도움이되지 않습니다. 내가 제안 할 수있는 유일한 것은 객체가 표현하고 싶은 최대 값에 대한 상징적 상수를 만들고 그것에 대해 테스트하는 것입니다.

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;


답변