コンパイラのビルトインを使用せずにオーバーフローセーフな追加を効率的に計算するCスニペットはありますか?


11

intオーバーフローが発生すると失敗する、別のに追加するC関数を次に示します。

int safe_add(int *value, int delta) {
        if (*value >= 0) {
                if (delta > INT_MAX - *value) {
                        return -1;
                }
        } else {
                if (delta < INT_MIN - *value) {
                        return -1;
                }
        }

        *value += delta;
        return 0;
}

残念ながら、GCCやClangでは十分に最適化されていません

safe_add(int*, int):
        movl    (%rdi), %eax
        testl   %eax, %eax
        js      .L2
        movl    $2147483647, %edx
        subl    %eax, %edx
        cmpl    %esi, %edx
        jl      .L6
.L4:
        addl    %esi, %eax
        movl    %eax, (%rdi)
        xorl    %eax, %eax
        ret
.L2:
        movl    $-2147483648, %edx
        subl    %eax, %edx
        cmpl    %esi, %edx
        jle     .L4
.L6:
        movl    $-1, %eax
        ret

このバージョンでは __builtin_add_overflow()

int safe_add(int *value, int delta) {
        int result;
        if (__builtin_add_overflow(*value, delta, &result)) {
                return -1;
        } else {
                *value = result;
                return 0;
        }
}

され、より良い最適化

safe_add(int*, int):
        xorl    %eax, %eax
        addl    (%rdi), %esi
        seto    %al
        jo      .L5
        movl    %esi, (%rdi)
        ret
.L5:
        movl    $-1, %eax
        ret

しかし、GCCまたはClangによってパターンマッチングされるビルトインを使用しない方法があるかどうか知りたいです。


1
乗算のコンテキストにgcc.gnu.org/bugzilla/show_bug.cgi?id=48580があることを確認します。ただし、パターンマッチングを行う方がはるかに簡単です。報告します。
Tavian Barnes

回答:


6

アーキテクチャのオーバーフローフラグにアクセスできない場合、私が思いついた最良の方法は、で行うことですunsigned。ここでは、すべてのビット演算について考えてみましょう。ここでは、最上位ビット(符号付きの値として解釈される場合の符号ビット)にのみ関心があることに注意してください。

(そのすべてのモジュロ符号エラー、私はこれを十分にチェックしませんでしたが、アイデアが明確であることを願っています)

#include <stdbool.h>

bool overadd(int a[static 1], int b) {
  unsigned A = a[0];
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  bool ret = (AeB & AuAB) > M;

  if (!ret) a[0] += b;
  return ret;
}

アトミックなものなど、UBが含まれていないバージョンの追加を見つけた場合、アセンブラにはブランチがありません(ただし、ロックプレフィックスがあります)。

#include <stdbool.h>
#include <stdatomic.h>
bool overadd(_Atomic(int) a[static 1], int b) {
  unsigned A = a[0];
  atomic_fetch_add_explicit(a, b, memory_order_relaxed);
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  bool ret = (AeB & AuAB) > M;
  return ret;
}

したがって、このような操作があったとしても、さらに「リラックス」すると、状況がさらに改善される可能性があります。

Take3:署名されていない結果から署名された結果への特別な「キャスト」を使用する場合、これはブランチフリーです。

#include <stdbool.h>
#include <stdatomic.h>

bool overadd(int a[static 1], int b) {
  unsigned A = a[0];
  //atomic_fetch_add_explicit(a, b, memory_order_relaxed);
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  unsigned res = (AeB & AuAB);
  signed N = M-1;
  N = -N - 1;
  a[0] =  ((AB > M) ? -(int)(-AB) : ((AB != M) ? (int)AB : N));
  return res > M;
}

2
DVではありませんが、2番目のXORは無効にしないでください。たとえばすべての提案をテストするこの試みを参照してください。
Bob__

私はこのようなものを試しましたが、それを機能させることができませんでした。有望に見えますが、GCCが慣用的なコードを最適化したいと思います。
R .. GitHub ICE HELPING ICE

1
@PSkocik、これは符号表現に依存しないので、計算は完全にとして行われunsignedます。しかし、それは、符号なしの型がマスクされた符号ビットだけではないという事実に依存します。(両方ともC2xで保証されています。つまり、私たちが見つけたすべてのアーチを保持します)。次に、unsignedそれがより大きい場合、結果をキャストバックすることはできません。これはINT_MAX、実装定義であり、シグナルを発生させる可能性があります。
イェンスガステッド

1
@PSkocik、残念ながらそうではありませんが、それは委員会に革命的なようでした。しかし、これは実際に私のマシンにブランチなしで出てくる「Take3」です。
イェンスガステッド

1
ご迷惑をおかけして申し訳ありませんが、正しい結果を得るにはTake3をこのようなものに変更する必要があると思います。しかし、それは有望なようです。
Bob__

2

符号付き演算の状況は、符号なし演算よりもはるかに悪く、符号付き加算のパターンは1つだけで、clangのみで、より広い型が使用可能な場合にのみ表示されます。

int safe_add(int *value, int delta)
{
    long long result = (long long)*value + delta;

    if (result > INT_MAX || result < INT_MIN) {
        return -1;
    } else {
        *value = result;
        return 0;
    }
}

clangは__builtin_add_overflow とまったく同じasmを提供します。

safe_add:                               # @safe_add
        addl    (%rdi), %esi
        movl    $-1, %eax
        jo      .LBB1_2
        movl    %esi, (%rdi)
        xorl    %eax, %eax
.LBB1_2:
        retq

そうでなければ、私が考えることができる最も簡単な解決策はこれです(イェンスが使用したインターフェイスを使用):

_Bool overadd(int a[static 1], int b)
{
    // compute the unsigned sum
    unsigned u = (unsigned)a[0] + b;

    // convert it to signed
    int sum = u <= -1u / 2 ? (int)u : -1 - (int)(-1 - u);

    // see if it overflowed or not
    _Bool overflowed = (b > 0) != (sum > a[0]);

    // return the results
    a[0] = sum;
    return overflowed;
}

gccとclangは非常によく似たasmを生成します。gccはこれを提供します:

overadd:
        movl    (%rdi), %ecx
        testl   %esi, %esi
        setg    %al
        leal    (%rcx,%rsi), %edx
        cmpl    %edx, %ecx
        movl    %edx, (%rdi)
        setl    %dl
        xorl    %edx, %eax
        ret

で合計を計算したいunsignedので、unsignedすべての値をint相互にくっつかずに表すことができる必要があります。結果をからunsignedに簡単に変換するにはint、その逆も役立ちます。全体として、2の補数が想定されます。

すべての一般的なプラットフォームで、からunsignedintの単純な割り当てで変換できると思いますint sum = u;が、Jensが述べたように、C2x標準の最新のバリアントでさえ、信号を発生させることができます。次の最も自然な方法は、そのようなことをすることです。*(unsigned *)&sum = u;しかし、パディングの非トラップバリアントは、明らかに、符号付き型と符号なし型で異なる可能性があります。したがって、上記の例は難しい方法です。幸い、gccとclangはどちらも、このトリッキーな変換を最適化します。

PS上記の2つのバリアントは動作が異なるため、直接比較できませんでした。最初の質問は元の質問に続き、*valueオーバーフローが発生した場合に失敗しません。2番目はJensからの回答に従い常に最初のパラメーターが指す変数を破棄しますが、それはブランチレスです。


生成されたasmを表示できますか?
R .. GitHub ICE HELPING ICEを停止する

gccでより良いasmを取得するために、オーバーフローチェックで等価をxorに置き換えました。asmを追加しました。
Alexander Cherepanov

1

私が思いつくことができる最高のバージョンは:

int safe_add(int *value, int delta) {
    long long t = *value + (long long)delta;
    if (t != ((int)t))
        return -1;
    *value = (int) t;
    return 0;
}

生成されるもの:

safe_add(int*, int):
    movslq  %esi, %rax
    movslq  (%rdi), %rsi
    addq    %rax, %rsi
    movslq  %esi, %rax
    cmpq    %rsi, %rax
    jne     .L3
    movl    %eax, (%rdi)
    xorl    %eax, %eax
    ret
.L3:
    movl    $-1, %eax
    ret

オーバーフローフラグを使用していなくても驚いています。明示的な範囲チェックよりもはるかに優れていますが、長いlongを追加することは一般化されていません。
Tavian Barnes

@TavianBarnesあなたがしている権利、残念ながら(コンパイラ固有の組み込み関数を除く)C言語で使用オーバフローフラグに良い方法はありません
イリヤBursov

1
このコードは、未定義の動作である符号付きオーバーフローの影響を受けます。
emacsは

@emacsdrivesmenuts、そうです、比較のキャストはオーバーフローする可能性があります。
イェンスガステッド

@emacsdrivesmenutsキャストは未定義ではありません。の範囲外の場合int、より広い型からのキャストは、実装定義の値を生成するか、シグナルを発生させます。私が気にするすべての実装は、正しいことを行うビットパターンを維持するためにそれを定義します。
Tavian Barnes

0

バイトを詰めずに2の補数表現を仮定(およびアサート)することで、コンパイラーに符号フラグを使用させることができます。そのような実装、コメントで注釈が付けられた行に必要な動作もたらすですが、この要件の明確な正式な確認が標準で見つかりません(おそらく何もありません)。

次のコードは、正の整数の加算のみを処理しますが、拡張できることに注意してください。

int safe_add(int* lhs, int rhs) {
    _Static_assert(-1 == ~0, "integers are not two's complement");
    _Static_assert(
        1u << (sizeof(int) * CHAR_BIT - 1) == (unsigned) INT_MIN,
        "integers have padding bytes"
    );
    unsigned value = *lhs;
    value += rhs;
    if ((int) value < 0) return -1; // impl. def., 6.3.1.3/3
    *lhs = value;
    return 0;
}

これは、clangとGCCの両方で生成されます。

safe_add:
        add     esi, DWORD PTR [rdi]
        js      .L3
        mov     DWORD PTR [rdi], esi
        xor     eax, eax
        ret
.L3:
        mov     eax, -1
        ret

比較でのキャストは未定だと思います。しかし、私の答えでそうするように、あなたはこれでうまくいくかもしれません。しかし、それから、すべての楽しみはすべてのケースをカバーできることです。あなたの_Static_assert目的の多くではないものであり、これは任意の現在のアーキテクチャ上の自明真実である、とさえC2Xのために課されますので。
イェンスガステッド

2
@Jens実際、私が(ISO / IEC 9899:2011)6.3.1.3/3を正しく読んでいる場合、キャストは実装定義であり、未定義ではないようです。それを再確認できますか?(ただし、これを負の引数に拡張すると、全体がかなり複雑になり、最終的にソリューションに似たものになります。)
Konrad Rudolph

あなたは正しい、それは実装が定義されていますが、シグナルを発生させる可能性もあります:(
Jens Gustedt

@Jensええ、技術的には2の補数の実装にはパディングバイトがまだ含まれている可能性があります。多分コードは理論的な範囲をと比較することによってこれをテストするべきINT_MAXです。投稿を編集します。しかし、繰り返しになりますが、このコードを実際に使用する必要はないと思います。
Konrad Rudolph、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.