そのため、C/C++の符号付き/符号なしの比較ルールを知っています。ここで、-1 > 2u == true
、および「正しい」比較を効率的に実装したい状況があります。
私の質問は、人々が知っている限り多くのアーキテクチャを考慮してより効率的です。明らかにIntelとARMのほうが重みが大きくなります。
与えられた:
int x;
unsigned int y;
if (x < y) {}
宣伝する方が良いですか:
x < y => (int64)x < (int64)y
または、2つの比較を実行する方が良いですか、つまり:
x < y => (x < 0 || x < y)
前者はゼロ拡張、符号拡張、および1つの比較+分岐を意味し、後者は符号拡張操作を必要とせず、2つの連続したcmp +分岐を必要とします。
伝統的な知恵では、分岐はパイプラインよりも符号拡張よりも高価であることが示唆されていますが、最初のケースでは拡張と単一の比較の間にストールがあります。 2つの比較をパイプライン処理しても、その後に2つの条件分岐が続く場合がありますか?
符号なしの値が符号付きの型よりも小さい型である別のケースがあります。つまり、単一のゼロ拡張で符号付きの型の長さを指定してから、単一の比較を行うことができます。 extend + cmpバージョンを使用するのか、それとも2つの比較方法がまだ推奨されていますか?
Intel?腕?その他?ここに正しい答えがあるかどうかはわかりませんが、人々の意見を聞きたいです。低レベルのパフォーマンスを予測することは、最近では特に困難です。特にIntelでは、ますますARMで予測が難しくなっています。
編集:
追加する必要があります。1つの明らかな解像度があります。型のサイズはアーキテクチャのint幅に等しくなります。その場合、プロモーション自体を効率的に実行できないため、2比較ソリューションが好ましいことは明らかです。明らかに私のint
の例は32ビットアーキテクチャのこの条件を満たしているため、32ビットプラットフォームに適用される演習用に思考実験をshort
に置き換えることができます。
編集2:
u
を忘れてしまいました-1 > 2u
! > _ <
編集3:
比較の結果が実際の分岐であり、結果がブール値として返されないことを前提に状況を修正したいと思います。これは、構造の外観を好む方法です。ただし、結果がブール対ブランチである場合、別の順列セットがあるという興味深い点があります。
int g;
void fun(int x, unsigned in y) { if((long long)x < (long long)y) g = 10; }
void gun(int x, unsigned in y) { if(x < 0 || x < y) g = 10; }
これは、if
に遭遇したときに通常暗示される意図したブランチを生成します;)
さて、あなたは正しく状況を類型化しました:C/C++には、単一の比較と完全な符号付き整数/符号なし整数の比較を行う方法がありません。
Int64への昇格が2つの比較を行うよりも速い場合、私は驚くでしょう。私の経験では、コンパイラーは、そのような部分式が純粋で(副作用がない)、したがって2番目のブランチが不要であることを理解するのに非常に優れています。 (ビット単位のORを使用して、短絡を明示的にオプトアウトすることもできます:(x < 0) | (x < y)
。)対照的に、私の経験では、コンパイラはネイティブのWordサイズより大きい整数に対して特別な場合の最適化をあまり行わない傾向があるため、(int64)x < (int64)y
は実際に完全なint比較を行う可能性が非常に高いです。
結論として、どのプロセッサでも可能な限り最高のマシンコードを生成することを保証する呪文はありませんが、最も一般的なプロセッサ上の最も一般的なコンパイラでは、2つの比較形式は昇格よりも遅くないと思います-int64形式。
編集:Godboltについていじくり回す人は、ARM32では、GCCがint64アプローチにあまりにも多くの機械を使いすぎることを確認しています。 VCはx86でも同じです。ただし、x64では、int64アプローチは実際には1命令分短くなります(昇格と64ビットの比較は簡単なため)。いずれにしても、 https://godbolt.org/g/wyG4yC
ケースバイケースでこれを判断する必要があります。プログラムで符号付きタイプが使用される理由はいくつかあります。
int
を入力するだけで、何も考えずに入力するだけです。int
型の0
などの整数定数を使用することで偶然に符号付きになりました。1)の場合、算術は符号付き算術で実行する必要があります。次に、期待される最大値を含めるために必要な最小の型に変換する必要があります。
たとえば、値が-10000
から10000
の範囲を持つことができるとします。次に、16ビットの符号付きタイプを使用して表現する必要があります。プラットフォームに依存せずに使用する正しいタイプは、int_fast16_t
です。
int_fastn_t
およびuint_fastn_t
型は、少なくともnと同じ大きさである必要がありますが、より高速なコード/より良いアライメントが得られる場合、コンパイラはより大きな型を選択できます。
2)stdint.h
を研究し、怠け者になるのをやめることで治癒します。プログラマーとしては、プログラムで宣言されたすべての単一変数のサイズと符号性を常に考慮する必要があります。これは、宣言の時点で行う必要があります。または、後で何らかの啓示を受けた場合は、戻ってタイプを変更してください。
型を慎重に検討しないと、多くの場合、しばしば微妙なバグを書くことになります。これはC++では特に重要です。C++では、Cよりも型の正確性が重視されます。
「ずさんなタイピング」が使用される場合、実際の意図されたタイプはほとんどの場合、署名されているのではなく、署名されていません。このずさんなタイピングの例を考えてみましょう:
for(int i=0; i<n; i++)
ここでsigned intを使用することはまったく意味がありません。なぜですか。ほとんどの場合、配列またはコンテナを反復処理しており、使用する正しい型はsize_t
です。
または、n
が保持できる最大サイズ(100など)がわかっている場合は、それに最適なタイプを使用できます。
for(uint_fast8_t i=0; i<100; i++)
3)勉強することでも治ります。特に、これらの言語に存在する暗黙的なプロモーションのさまざまなルール(通常の算術変換および整数プロモーションなど)。
あなたが提示した特定のセットアップを考えると:
int x;
unsigned int y;
そして、x
の値がy
の値よりも数値的に小さいかどうかを評価するあなたの明白な意図は、x
の符号を尊重して、
if ((x < 0) || (x < y)) {}
つまり、2番目の選択肢です。 y
の型の最大表現可能値が少なくともx
の型の最大表現可能値と同じ大きさである限り、意図を明確に表現し、より広い型に拡張可能です。したがって、引数がその形式を持つことを指定したい場合は、それを-目をそらし、C++の支持者-マクロとして書くことさえできます。
両方の引数を符号付き64ビット整数型に変換することは、実際にはint
またはunsigned int
からpromotionになるという保証がないため、移植性のあるソリューションではありません。また、より広い型には拡張できません。
あなたの2つの選択肢の相対的なパフォーマンスに関しては、大きな違いがあるとは思いませんが、それがあなたにとって重要であるなら、慎重なベンチマークを書きたいでしょう。他のマシン命令よりも機械命令を1つ多く必要とするポータブルな選択肢を想像できますし、1つ少ない機械命令を必要とすることも想像できます。そのような比較がアプリケーションのパフォーマンスを支配する場合にのみ、単一の命令が何らかの形で顕著な違いをもたらします。
もちろん、これはあなたが提示した状況に固有のものです。コンパイル時にソートされた多くの異なるタイプについて、符号付き/符号なしの混合比較をいずれかの順序で処理したい場合、テンプレートベースのラッパーがそれを助けます(そしてそれはマクロを使用する問題を解きます)、ただし、比較自体の詳細については具体的にお尋ねします。
2ブランチバージョンは確かに遅くなりますが、実際には、x86の2ブランチ...またはシングルブランチ...のいずれでもありません。
たとえば、x86 gcc 7.1はC++ソースに対して次のようになります。
bool compare(int x, unsigned int y) {
return (x < y); // "wrong" (will emit warning)
}
bool compare2(int x, unsigned int y) {
return (x < 0 || static_cast<unsigned int>(x) < y);
}
bool compare3(int x, unsigned int y) {
return static_cast<long long>(x) < static_cast<long long>(y);
}
このアセンブリを作成します( godbolt live demo ):
compare(int, unsigned int):
cmp edi, esi
setb al
ret
compare2(int, unsigned int):
mov edx, edi
shr edx, 31
cmp edi, esi
setb al
or eax, edx
ret
compare3(int, unsigned int):
movsx rdi, edi
mov esi, esi
cmp rdi, rsi
setl al
ret
そして、より複雑なコード内でこれらを使用しようとすると、99%のケースでインライン化されます。プロファイリングせずに推測するだけですが、「ガットで」私はcompare3
を「高速」として使用します。特に、コード内で順不同で実行される場合(面白いのは、 uint引数、esi
の上位32bのいくつかの混乱と比較するコード呼び出しを生成するためにかなりの努力が必要になりますが、より複雑な計算でインライン化された場合、おそらくそれを取り除くでしょう引数も既にuint64で拡張されているため、compare3
はさらに単純+短くなります)。
...コメントで述べたように、これが必要なタスクはヒットしません。たとえば、有効なデータの範囲が不明な場所で作業することは想像できないので、C/C++は完璧にフィットし、その動作方法を正確に評価します(署名された型と署名されていない型の<
が明確に定義されており、最短/最速のコードが生成され、さらに検証する責任があるプログラマーになるように警告が発せられます、およびソースを適切に変更する必要がある場合)。
できるポータブルな方法の1つは、両方の引数をintmax_t
から<stdint.h>
に拡張できるかどうかを確認することです。これは、実装がサポートする最も広い整数型です。 (sizeof(intmax_t) > sizeof(x) && sizeof(intmax_t) >= sizeof(y))
を確認し、そうであれば、拡大変換を行うことができます。これは、int
が32ビット幅で、long long int
が64ビット幅である非常に一般的なケースで機能します。
C++では、引数のstd::numeric_limits<T>
をチェックする安全な比較テンプレートがある賢いことを行うことができます。 1つのバージョンがあります。 (gccまたはclangで-Wno-sign-compare
を使用してコンパイルしてください!)
#include <cassert>
#include <cstdint>
#include <limits>
using std::intmax_t;
using std::uintmax_t;
template<typename T, typename U>
inline bool safe_gt( T x, U y ) {
constexpr auto tinfo = std::numeric_limits<T>();
constexpr auto uinfo = std::numeric_limits<U>();
constexpr auto maxinfo = std::numeric_limits<intmax_t>();
static_assert(tinfo.is_integer, "");
static_assert(uinfo.is_integer, "");
if ( tinfo.is_signed == uinfo.is_signed )
return x > y;
else if ( maxinfo.max() >= tinfo.max() &&
maxinfo.max() >= uinfo.max() )
return static_cast<intmax_t>(x) > static_cast<intmax_t>(y);
else if (tinfo.is_signed) // x is signed, y unsigned.
return x > 0 && x > y;
else // y is signed, x unsigned.
return y < 0 || x > y;
}
int main()
{
assert(-2 > 1U);
assert(!safe_gt(-2, 1U));
assert(safe_gt(1U, -2));
assert(safe_gt(1UL, -2L));
assert(safe_gt(1ULL, -2LL));
assert(safe_gt(1ULL, -2));
}
2行を変更することで、浮動小数点を認識できます。
小さなテンプレートジグリーポケリーで、すべてのシナリオで自動的に最適な結果を得ることができると思います。
#include<iostream>
#include<cassert>
template<class T> auto make_unsigned(T i) -> T { return i; }
auto make_unsigned(int i) -> unsigned int {
assert(i >= 0);
return static_cast<unsigned int>(i);
}
auto make_unsigned(short i) -> unsigned short {
assert(i >= 0);
return static_cast<unsigned short>(i);
}
auto make_unsigned(long long i) -> unsigned long long {
assert(i >= 0);
return static_cast<unsigned long long>(i);
}
template<
class I1,
class I2,
std::enable_if_t<(std::is_signed<I1>::value and std::is_signed<I2>::value)
or (not std::is_signed<I1>::value and not std::is_signed<I2>::value)>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {
return i1 < i2;
};
template<
class I1,
class I2,
std::enable_if_t<std::is_signed<I1>::value and not std::is_signed<I2>::value>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {
return (i1 < 0) or make_unsigned(i1) < i2;
};
template<
class I1,
class I2,
std::enable_if_t<not std::is_signed<I1>::value and std::is_signed<I2>::value>* = nullptr
>
bool unsigned_less(I1 i1, I2 i2) {
return not (i2 < 0) and i1 < make_unsigned(i2);
};
int main() {
short a = 1;
unsigned int b = 2;
std::cout << unsigned_less(a, b) << std::endl;
using uint = unsigned int;
using ushort = unsigned short;
std::cout << unsigned_less(ushort(1), int(3)) << std::endl;
std::cout << unsigned_less(int(-1), uint(0)) << std::endl;
std::cout << unsigned_less(int(1), uint(0)) << std::endl;
return 0;
}
イントロスペクションによるデザインに関するベルリンでの最近のDカンファレンスでのAndrei Alexandrescusの基調講演をご覧ください。
その中で、彼はDESIGN時にチェックされたintクラスを設計する方法を示しています。彼が思いついた機能の1つはまさにこれです-署名済みと未署名を比較する方法です。
基本的に、2つの比較を実行する必要があります
If(signed_var <0)の場合はunsigned_varを返し、そうでない場合はsigned_varをunsigned_varに昇格/キャストしてから比較します