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


24

문맥

원래 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 비트 결과를 제공합니다

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


14
"비트 보수와 같은 연산이 피연산자와 동일한 크기의 결과를 반환해야한다는 가정이 맞습니까?" 아니요, 이것은 정확하지 않으며 정수 프로모션이 적용됩니다.
Thomas Jager

2
확실히 관련이 있지만 문제에 대한 해결책을 제공하지 않기 때문에이 특정 질문과 중복된다고 확신하지 않습니다.
코디 그레이

3
나는 미친 약을 복용하고 있다고 느끼고 C는 이것보다 조금 이식성이 있어야합니다. 8 비트 유형에서 정수 승격을 얻지 못하면 컴파일러는 C 표준과 호환되지 않았습니다. 이 경우 모든 계산을 통해 확인하고 필요한 경우 수정 해야한다고 생각합니다 .
user694733

1
정말 중요하지 않은 카운터를 제외하고 어떤 논리가 "충분한 공간이 있다면 증가시킬 수 있습니다. 코드를 포팅하는 경우 uint_8 대신 int (4 바이트)를 사용할 수 있습니까? 많은 경우에 문제를 예방할 수 있습니다.

1
@puck 맞습니다. 4 바이트로 변경할 수 있지만 기존 시스템과 통신 할 때 호환성이 떨어집니다. 의도는 언제 오류 가 있는지 알고 1 바이트 카운터가 원래 충분했으며 그 상태를 유지하는 것입니다.
Charlie Salts

답변:


26

당신이보고있는 것은 정수 프로모션 의 결과입니다 . 식에 정수 값이 사용되는 대부분의 경우 값의 유형이 값보다 작 으면 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으로 줄이면 가장 큰 양수 값을 얻습니다.


1
if (!++counter) --counter;쉼표 연산자를 사용하는 것보다 일부 프로그래머에게는 덜 이상 할 수 있습니다.
Eric Postpischil

1
또 다른 대안은 ++counter; counter -= !counter;입니다.
Eric Postpischil

@EricPostpischil 사실, 첫 번째 옵션이 더 좋습니다. 편집했습니다.
dbush

15
어떻게 작성하든 추악하고 읽을 수 없습니다. 이 같은 관용구를 사용해야하는 경우, 모든 유지 보수 프로그래머에게 부탁을하고 인라인 함수로 마무리 : 같은 increment_unsigned_without_wraparound또는 increment_with_saturation. 개인적으로, 나는 일반적인 tri-operand clamp함수를 사용할 것 입니다.
코디 그레이

5
또한 함수는 인수 유형에 따라 다르게 동작해야하므로 함수로 만들 수 없습니다. 유형이 일반적인 매크로 를 사용해야합니다 .
user2357112는

7

에서 는 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


3
이 질문에는 명시 적으로 처리 할 수있는 여러 크기가 있으므로 != 255부적절합니다.
Eric Postpischil

@EricPostpischil 아 네, 잊어 버려서 항상 "sign!
bruno

1
즉에 대한 정의되지 않습니다 unsignedC 표준에 의해 정의되지 않은 폭 전체 개체의 변화 이후 객체 있지만 고정 할 수 있습니다 (2u << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil

오 예 ofc, CHAR_BIT, 나의 나쁜
bruno

2
더 넓은 유형의 안전을 위해을 사용할 수 있습니다 ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil

5

다음은 부호없는 정수 유형 인 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. 그러나 많은 아키텍처에서는 분할이 다소 느립니다. 컴파일러는 컴파일러와 대상 아키텍처에 따라 나누지 않고 명령어에 맞게이를 최적화 할 수 있습니다.


1
매크로 사용을 권장하는 답변을 공표 할 수는 없습니다. C는 인라인 함수를 가지고 있습니다. 인라인 함수 내에서 쉽게 수행 할 수없는 매크로 정의 내부에서는 아무것도하지 않습니다. 매크로를 사용하려는 경우 명확성을 위해 전략적으로 괄호를 사용해야합니다. 연산자 <<의 우선 순위는 매우 낮습니다. Clang은 (으)로 이에 대해 경고합니다 -Wshift-op-parentheses. 좋은 소식, 좋은 최적화 컴파일러입니다 하지 당신이 느린 것에 대해 걱정하지 않아도, 여기 사업부를 생성하는 것이다.
코디 그레이

1
@CodyGray, 함수로 이것을 할 수 있다고 생각되면 답을 쓰십시오.
Carsten S

2
@CodyGray : 일부 고정 유형의 매개 변수 (또는 다른 표현식) 여야 sizeof x하므로 C 함수 내에서 구현할 수 없습니다 x. 호출자가 사용하는 인수 유형의 크기를 생성 할 수 없습니다. 매크로는 할 수 있습니다.
Eric Postpischil

2

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 또는 그와 비슷한 것을 수행하십시오. 컴파일러는 이러한 솔루션 중 하나와 동일한 코드로 코드를 최적화하여 더 읽기 쉽고 덜 위험하게 만듭니다. ...

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


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

C 2011 온라인 초안

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

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

if ( counter + 1 ) counter++;

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

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

정수 승격에 대한 요점에 감사드립니다. 이것이 우리가 겪고있는 문제 인 것 같습니다. 그러나 두 번째 코드 샘플에서는 -1카운터가 254 (0xFE)로 설정되므로 필요하지 않습니다. 어쨌든,이 질문에 언급 된 것처럼이 접근법은이 관용구에 참여하는 코드베이스의 다양한 크기로 인해 이상적이지 않습니다.
Charlie Salts
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.