부호없는 바이트에 대한 포화 빼기 / 더하기


83

두 개의 unsigned 바이트 bx. bsubb - xbadd로 계산해야합니다 b + x. 그러나 이러한 작업 중에 언더 플로 / 오버플로가 발생하는 것을 원하지 않습니다. 예 (의사 코드) :

b = 3; x = 5;
bsub = b - x; // bsub must be 0, not 254

b = 250; x = 10;
badd = b + x; // badd must be 255, not 4

이를 수행하는 명백한 방법에는 분기가 포함됩니다.

bsub = b - min(b, x);
badd = b + min(255 - b, x);

이 작업을 수행하는 더 좋은 방법이 있는지 궁금합니다. 즉, 일부 해키 비트 조작으로?


13
y ^ ((x ^ y) & -(x < y))위한 int유형 평가 min(x, y)분기없이. 이것은 지금까지 가지고있는 것을 기반으로 최종 솔루션의 일부를 형성 할 수 있습니다.
밧세바

3
아마도 클램프 증가 정수? 도움이됩니다.
Shafik Yaghmour 2015

8
C 또는 C ++ 질문입니까? 하나를 선택하십시오.
fuz

9
@AlanCampbell은 Saturating Arithmetic 이라고 합니다.
Shafik Yaghmour 2015

7
휴대 할 수 있어야합니까? 특정 아키텍처를보고 있다면 멋진 단일 지침이있을 것입니다. ARM에는 바이트에 대한 포화 벡터 더하기 및 빼기가 있습니다. X86에서 _mm_adds_epi8내장 함수는 단일 명령어에서 16 바이트의 포화 추가를 수행합니다.
porglezomp 2015

답변:


86

Branchfree Saturating Arithmetic 기사는 이에 대한 전략을 제공합니다.

추가 솔루션은 다음과 같습니다.

u32b sat_addu32b(u32b x, u32b y)
{
    u32b res = x + y;
    res |= -(res < x);

    return res;
}

uint8_t에 대해 수정 됨 :

uint8_t  sat_addu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x + y;
    res |= -(res < x);

    return res;
}

뺄셈 솔루션은 다음과 같습니다.

u32b sat_subu32b(u32b x, u32b y)
{
    u32b res = x - y;
    res &= -(res <= x);

    return res;
}

uint8_t에 대해 수정 됨 :

uint8_t sat_subu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x - y;
    res &= -(res <= x);

    return res;
}

2
@ user1969104 일 수 있지만 기사의 주석에서 알 수 있듯이 단항 마이너스를 적용하기 전에 unsigned로 캐스팅하여 해결됩니다. 실제로 는 2의 보수 외에는 다른 것을 다룰 필요가 없을 것입니다 .
Shafik Yaghmour 2015

2
이것은 좋은 C 대답 일 수 있지만 좋은 C ++ 대답은 아닙니다.
Yakk-Adam Nevraumont 2015

4
@Yakk 이것이 "나쁜"C ++ 대답을 만드는 이유는 무엇입니까? 이것들은 기본적인 수학적 연산이며 C로만 해석되거나 나쁜 C ++로 어떻게 해석되는지 모르겠습니다.
JPhi1618 2015

4
@ JPhi1618 더 나은 C ++ 대답은 template<class T>struct sat{T t;};포화되는 오버로드 된 연산자 일 수 있습니다 . 네임 스페이스의 적절한 사용. 주로 설탕.
Yakk-Adam Nevraumont 2015

6
@Yakk, 아, 알았어. 나는 이것을 OP가 필요에 따라 조정할 수있는 최소한의 예제로 보았다. 나는 완전한 구현을 기대하지 않습니다. 명확히 해주셔서 감사합니다.
JPhi1618 2015

40

간단한 방법은 오버플로를 감지하고 그에 따라 값을 재설정하는 것입니다.

bsub = b - x;
if (bsub > b)
{
    bsub = 0;
}

badd = b + x;
if (badd < b)
{
    badd = 255;
}

GCC는 -O2로 컴파일 할 때 오버 플로우 검사를 조건부 할당으로 최적화 할 수 있습니다.

다른 솔루션과 비교하여 최적화 정도를 측정했습니다. 내 PC에서 1000000000+ 작업을 수행 할 때이 솔루션과 @ShafikYaghmour의 솔루션은 평균 4.2 초 였고 @chux의 솔루션은 평균 4.8 초였습니다. 이 솔루션은 또한 더 읽기 쉽습니다.


5
@ user694733 멀리 최적화되지 않았으며 캐리 플래그에 따라 조건부 할당으로 최적화되었습니다.
fuz

2
네, user694733이 맞습니다. 조건부 할당으로 최적화됩니다.
user1969104 2015

예를 들어 badd : b = 155 x = 201, badd = 156보다, b보다 큽니다. 결과를 연산에 따라 두 변수의 min () 또는 max ()와 비교해야합니다
Cristian F

@CristianF 155 + 201 = 156을 어떻게 계산합니까? 나는 그것이 155 + 201 = 356 % 256 = 100 일 필요가 있다고 생각한다. 나는 min (), max ()가 b, x 값의 어떤 조합에도 필요하다고 생각하지 않는다.
user1969104

16

빼기 :

diff = (a - b)*(a >= b);

부가:

sum = (a + b) | -(a > (255 - b))

진화

// sum = (a + b)*(a <= (255-b)); this fails
// sum = (a + b) | -(a <= (255 - b)) falis too

@R_Kapp 덕분에

@NathanOliver 덕분에

이 연습은 단순히 코딩의 가치를 보여줍니다.

sum = b + min(255 - b, a);

대한 sum아마 (a + b) | -(a <= (255 - b))?
R_Kapp 2015

당신은 할 수sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFF가정 sizeof(int) > sizeof(unsigned char),하지만 당신은 (두통 이외의) 그것으로 무엇을 얻을 것인지 내가 모르는이 모습 너무 복잡.
user694733 2015

@ user694733 예 그리고 어쩌면 (a+b+1)*(a <= (255-b)) - 1.
chux-Monica 복원

@NathanOliver 감독 해 주셔서 감사합니다-이것의 말 sub은 한계가 너무 쉬웠다 는 것 0입니다. 그러나 다른 제한은 복잡하고 user2079303의 의견을 따릅니다 .
chux-Monica 복원

1
@ user1969104 OP는 "더 나은"(코드 공간 대 속도 성능), 대상 플랫폼 또는 컴파일러에 대해 명확하지 않았습니다. 속도 평가는 게시되지 않은 더 큰 문제의 맥락에서 가장 의미가 있습니다.
chux-Monica 복원

13

최신 버전의 gcc 또는 clang (다른 버전 일 수도 있음)을 사용하는 경우 내장 기능 을 사용하여 오버플로를 감지 할 수 있습니다 .

if (__builtin_add_overflow(a,b,&c))
{
  c = UINT_MAX;
}

이것이 최고의 답변입니다. 비트 매직 대신 컴파일러 내장을 사용하는 것이 더 빠를뿐만 아니라 더 명확하고 코드를 더 관리하기 쉽게 만듭니다.
두족류

감사합니다, @erebos. 사용 가능한 플랫폼에서 확실히 시도해 보겠습니다.
ovk

3
나는 이것으로 brachless 코드를 생성하기 위해 gcc를 얻을 수 없다. 이것은 약간 실망 스럽다. 여기서 특히 불행한 점은 clang이 이들에 대해 다른 이름을 사용 한다는 것 입니다.
Shafik Yaghmour

1
@Cephalopod 그리고 그것은 완전히 크로스 플랫폼이 아닙니다. 도대체 다른 컴파일러에서도 작동하지 않을 가능성이 큽니다. 21 세기에 좋은 해결책이 아닙니다.
Ela782

1
@ Ela782 정확히 반대입니다. 내장 기능은 20 세기에 좋은 솔루션이 아닙니다. 미래에 오신 것을 환영합니다!
두족류

3

추가 :

unsigned temp = a+b;  // temp>>8 will be 1 if overflow else 0
unsigned char c = temp | -(temp >> 8);

빼기 :

unsigned temp = a-b;  // temp>>8 will be 0xFF if neg-overflow else 0
unsigned char c = temp & ~(temp >> 8);

비교 연산자 나 곱셈이 필요하지 않습니다.


3

어셈블리 또는 내장 함수를 사용하려는 경우 최적의 솔루션이 있다고 생각합니다.

빼기 :

우리는 지시를 사용할 수 있습니다sbb

MSVC에서는 내장 함수 _subborrow_u64 (다른 비트 크기에서도 사용 가능)를 사용할 수 있습니다.

사용 방법은 다음과 같습니다.

// *c = a - (b + borrow)
// borrow_flag is set to 1 if (a < (b + borrow))
borrow_flag = _subborrow_u64(borrow_flag, a, b, c);

귀하의 상황에 적용 할 수있는 방법은 다음과 같습니다.

uint64_t sub_no_underflow(uint64_t a, uint64_t b){
    uint64_t result;
    borrow_flag = _subborrow_u64(0, a, b, &result);
    return result * !borrow_flag;
}

추가 :

우리는 지시를 사용할 수 있습니다adcx

MSVC에서는 내장 함수 _addcarry_u64 를 사용할 수 있습니다 (다른 비트 크기에서도 사용 가능).

사용 방법은 다음과 같습니다.

// *c = a + b + carry
// carry_flag is set to 1 if there is a carry bit
carry_flag = _addcarry_u64(carry_flag, a, b, c);

귀하의 상황에 적용 할 수있는 방법은 다음과 같습니다.

uint64_t add_no_overflow(uint64_t a, uint64_t b){
    uint64_t result;
    carry_flag = _addcarry_u64(0, a, b, &result);
    return !carry_flag * result - carry_flag;
}

뺄셈만큼 좋아하지는 않지만 꽤 멋지다고 생각합니다.

추가가 오버플로되면 carry_flag = 1. Not-ing carry_flag은 0을 산출하므로 !carry_flag * result = 0오버플로가 발생합니다. 그리고 0 - 1부호없는 정수 값을 최대 값으로 설정하기 때문에 함수는 캐리가 없으면 더하기 결과를 반환하고 캐리가 있으면 선택한 정수 값의 최대 값을 반환합니다.


1
(? 86)이 대답은 특정 명령어 세트 아키텍처에 대한 것을 언급 할 수 있으며 각 대상 아키텍처 (SPARC, MIPS, ARM 등)에 대한 재 구현이 필요합니다
토비 Speight

2

이것에 대해 :

bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;

bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;

(명백한?) 오타를 수정했지만 여전히 이것이 옳다고 생각하지 않습니다.
밧세바

여기에는 분기도 포함됩니다.
fuz 2015

삼항 연산자와 if / else 문의 차이점은 무엇입니까?

@GRC 차이가 없습니다.
fuz 2015

@GRC FUZxxl이 맞지만 항상 그렇듯이 스스로 시도하십시오. 어셈블리를 모르더라도 (당신에게 명확하지 않은 것이 있으면 여기에서 질문을 할 수 있습니다), 당신이 알게 될 길이 / 지침을 확인하십시오.
edmz 2015

2

모두 부호없는 바이트 산술로 수행 할 수 있습니다.

// Addition without overflow
return (b > 255 - a) ? 255 : a + b

// Subtraction without underflow
return (b > a) ? 0 : a - b;

1
이것은 실제로 최고의 솔루션 중 하나입니다. 이전에 빼기 또는 더하기를 수행하는 다른 모든 작업은 실제로 C ++에서 정의되지 않은 동작을 생성하므로 컴파일러가 원하는대로 수행 할 수 있습니다. 실제로 일어날 일을 대부분 예측할 수 있지만 여전히 그렇습니다.
Adrien Hamelin 2015

2

2 바이트로이 작업을 수행하려면 가능한 가장 간단한 코드를 사용하십시오.

20 억 바이트로이 작업을 수행하려면 프로세서에서 사용할 수있는 벡터 명령과 사용할 수 있는지 확인하십시오. 프로세서가 단일 명령으로 이러한 작업 중 32 개를 수행 할 수 있음을 알 수 있습니다.


2

Boost Library Incubator 에서 안전한 숫자 라이브러리를 사용할 수도 있습니다 . int, long 등에 대한 드롭 인 교체를 제공하여 감지되지 않은 오버플로, 언더 플로 등이 발생하지 않도록 보장합니다.


7
라이브러리 사용 방법에 대한 예를 제공하면 더 나은 답변이 될 것입니다. 또한 그들은 brachless 보증을 제공합니까?
Shafik Yaghmour 2015

라이브러리에는 광범위한 문서와 예제가 있습니다. 그러나 하루가 끝나면 적절한 헤더를 포함하고 int를 safe <int>로 대체하는 것만 큼 쉽습니다.
Robert Ramey 2015

가지가 없습니까? 나는 당신이 가지가없는 사람이라고 생각합니다. 라이브러리는 템플릿 메타 프로그래밍을 사용하여 필요한 경우에만 런타임 검사를 포함합니다. 예를 들어 unsigned char 곱하기 unsigned char은 unsigned int가됩니다. 이것은 절대로 넘칠 수 없으므로 검사가 전혀 필요하지 않습니다. 반면에 unsigned 시간 unsigned는 오버플로 될 수 있으므로 런타임에 확인해야합니다.
Robert Ramey 2015

1

이러한 메서드를 많이 호출 할 경우 가장 빠른 방법은 비트 조작이 아니라 조회 테이블 일 것입니다. 각 작업에 대해 길이 511의 배열을 정의하십시오. 마이너스 (빼기)의 예

static unsigned char   maxTable[511];
memset(maxTable, 0, 255);           // If smaller, emulates cutoff at zero
maxTable[255]=0;                    // If equal     - return zero
for (int i=0; i<256; i++)
    maxTable[255+i] = i;            // If greater   - return the difference

배열은 정적이며 한 번만 초기화됩니다. 이제 뺄셈을 인라인 방법이나 사전 컴파일러를 사용하여 정의 할 수 있습니다.

#define MINUS(A,B)    maxTable[A-B+255];

어떻게 작동합니까? 서명되지 않은 문자에 대해 가능한 모든 뺄셈을 미리 계산하고 싶습니다. 결과는 -255에서 +255까지 다양하며 총 511 개의 결과가 있습니다. 가능한 모든 결과의 배열을 정의하지만 C에서는 음수 인덱스에서 액세스 할 수 없기 때문에 +255 ([A-B + 255]에서)를 사용합니다. 배열의 중심에 대한 포인터를 정의하여이 작업을 제거 할 수 있습니다.

const unsigned char *result = maxTable+255;
#define MINUS(A,B)    result[A-B];

다음과 같이 사용하십시오.

bsub  = MINUS(13,15); // i.e 13-15 with zero cutoff as requested

실행이 매우 빠릅니다. 결과를 얻기 위해 단 하나의 빼기와 하나의 포인터를 따릅니다. 분기가 없습니다. 정적 배열은 매우 짧기 때문에 계산 속도를 더욱 높이기 위해 CPU 캐시에 완전히로드됩니다.

덧셈에도 동일하게 작동하지만 약간 다른 테이블을 사용합니다 (처음 256 개 요소는 인덱스가되고 마지막 255 개 요소는 255 개 이상의 컷오프를 에뮬레이트하기 위해 255 개와 같습니다.

비트 연산을 고집하면 (a> b)를 사용하는 대답이 잘못되었습니다. 이것은 여전히 ​​분기로 구현 될 수 있습니다. 부호 비트 기술 사용

// (num1>num2) ? 1 : 0
#define        is_int_biggerNotEqual( num1,num2) ((((__int32)((num2)-(num1)))&0x80000000)>>31)

이제 뺄셈과 덧셈 계산에 사용할 수 있습니다.

분기없이 max (), min () 함수를 에뮬레이션하려면 다음을 사용하십시오.

inline __int32 MIN_INT(__int32 x, __int32 y){   __int32 d=x-y; return y+(d&(d>>31)); }              

inline __int32 MAX_INT(__int32 x, __int32 y){   __int32 d=x-y; return x-(d&(d>>31)); }

위의 예에서는 32 비트 정수를 사용합니다. 32 비트 계산이 조금 더 빨리 실행된다고 생각하지만 64로 변경할 수 있습니다. 당신까지


2
실제로는 그렇지 않을 것입니다. 첫째, 테이블을로드하는 속도가 느립니다. 비트 작업은 1주기가 걸리며 메모리에서로드하는 데는 약 80ns가 걸립니다. L1 캐시에서도 우리는 3GHz CPU에서 거의 7주기 인 20ns 범위에 있습니다.
edmz 2015

당신은 전적으로 정확하지 않습니다. LUT 방법은 몇 가지주기가 필요하지만 비트 조작도 단일주기가 아닙니다. 몇 가지 순차적 인 작업이 있습니다. 예를 들어, MAX ()를 계산하는 데만 2 개의 빼기와 논리 연산과 1 개의 오른쪽 시프트가 필요합니다. 정수 승격 / 강등을 잊지 마세요.
DanielHsH 2015

1
단일 비트 연산은 자연스럽게 레지스터 피연산자를 가정하여 1 사이클이 걸린다는 것을 의미했습니다. Shafik이 보여준 코드로 clang은 4 개의 기본 명령어를 출력합니다. 또한 (x > y)분기가 없습니다.
edmz 2015

첫째, (x> y)는 분기를 사용할 수 있습니다. 어떤 아키텍처에서 실행 중인지 모릅니다. 나는 인텔 아키텍처에서 분기가 없을 가능성이 있다는 데 동의하는 경향이 있습니다. 대부분의 스마트 폰은 인텔이 아닙니다. 그것이 얼마나 많은 조립 지침이 있을지 알 수없는 이유이기도합니다. PC에서 내 솔루션을 사용해보십시오. 결과를 듣고 싶습니다.
DanielHsH 2015

1
L1 캐시는 20ns보다 훨씬 빠르며 약 4 개의 프로세서주기 정도입니다. 그리고 사용되지 않는 실행 단위를 사용할 가능성이 높으며 어쨌든 완전히 파이프 라인됩니다. 그것을 측정하십시오. 그리고 20ns는 3GHz CPU에서 60 사이클입니다.
gnasher729 2015
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.