少し前に、私の古い先生がこのコードを投稿しました。これは、配列を同じ数字(もちろんゼロ以外)に初期化する別の方法だと言っています。
この場合は3つです。
彼は、この方法がfor
ループよりわずかに優れていると言いました。左シフト演算子が必要なのはなぜですか?別の長い配列が必要なのはなぜですか?ここで何が起きているのか理解できません。
int main() {
short int A[100];
long int v = 3;
v = (v << 16) + 3;
v = (v << 16) + 3;
v = (v << 16) + 3;
long *B = (long*)A;
for(int i=0; i<25; i++)
B[i] = v;
cout << endl;
print(A,100);
}
彼はlong
がshort
の4倍長いと仮定しています(これは保証されていません。int16_tとint64_tを使用する必要があります)。
彼はその長いメモリ空間(64ビット)を取得し、4つの短い(16ビット)値で埋めます。彼は、ビットを16スペース分シフトして値を設定しています。
その後、彼はshortの配列をlongの配列として扱いたいので、100回ではなく25回のループ反復を行うことで100個の16ビット値を設定できます。
それが先生の考え方ですが、他の人が言ったように、このキャストは未定義の動作です。
配列に同じ値を設定するには多くの方法があり、パフォーマンスが心配な場合は測定する必要があります。
C++には配列に値を入力するための専用関数があり、これを使用します(#include <algorithm>
および#include <iterator>
):
std::fill(std::begin(A), std::end(A), 3);
最適化コンパイラがこのようなことで何ができるかを過小評価しないでください。
コンパイラーが何をするのか興味があるなら、Matt Godboltの Compiler Explorer は、少しのアセンブラーを学ぶ準備ができていれば非常に良いツールです。 here からわかるように、コンパイラーはfill
呼び出しを最適化し、ループを展開して12(およびビット)128ビットストアを呼び出すことができます。コンパイラーはターゲット環境に関する知識を持っているため、ソースコード内のターゲット固有の仮定をエンコードせずにこれを行うことができます。
ホグウォッシュの絶対的な負荷。
まず、v
はコンパイル時で計算されます。
タイプが関連していないため、long *B = (long*)A;
に続くB
の逆参照の動作は未定義です。 B[i]
はB
の逆参照です。
long
がshort
の4倍であるという仮定には何の正当化もありません。
for
ループを簡単な方法で使用し、コンパイラーが最適化することを信頼してください。砂糖を上に乗せてください。
質問にはC++タグ(Cタグなし)があるため、これはC++スタイルで行う必要があります。
// C++ 03
std::vector<int> tab(100, 3);
// C++ 11
auto tab = std::vector<int>(100, 3);
auto tab2 = std::array<int, 100>{};
tab2.fill(3);
また、教師は驚くべきことを行うことができるコンパイラーをしのいでいます。適切に設定されていれば、コンパイラがあなたのためにそれを行うことができるので、そのようなトリックを行う意味はありません:
ご覧のとおり、-O2
結果コードは(ほぼ)各バージョンで同じです。 -O1
の場合、トリックによりいくらか改善されます。
結論として、あなたは選択をしなければなりません:
-O2
を使用しますGodboltサイトを使用して、他のコンパイラと構成を試してください。 最新のcppConトーク も参照してください。
他の回答で説明されているように、コードは型エイリアスの規則に違反しており、標準で保証されていない仮定を行っています。
手動でこの最適化を本当に行いたい場合、これは明確に定義された振る舞いを持つ正しい方法です。
long v;
for(int i=0; i < sizeof v / sizeof *A; i++) {
v = (v << sizeof *A * CHAR_BIT) + 3;
}
for(int i=0; i < sizeof A / sizeof v; i++) {
std:memcpy(A + i * sizeof v, &v, sizeof v);
}
オブジェクトのサイズに関する安全でない仮定はsizeof
を使用することで修正され、エイリアス違反はstd::memcpy
を使用することで修正されました。
そうは言っても、コードを単純に保ち、代わりにコンパイラーに魔法をかけるのがおそらく最善です。
左シフト演算子が必要なのはなぜですか?
ポイントは、大きい整数を小さい整数の複数のコピーで埋めることです。 2バイトの値s
を大きな整数l
に書き込むと、shift 2バイトの残りのビット(私の固定バージョンでは、マジックナンバーの由来)その後、値s
を構成するバイトの2つのコピーを持つ整数が得られます。 l
のすべてのバイトペアが同じ値に設定されるまで、これが繰り返されます。シフトを行うには、シフト演算子が必要です。
これらの値が2バイト整数の配列を含む配列にコピーされると、1回のコピーで複数のオブジェクトの値がより大きなオブジェクトのバイトの値に設定されます。バイトの各ペアは同じ値を持つため、配列の小さい整数も同じ値になります。
なぜ
long
の別の配列が必要なのですか?
long
の配列はありません。 short
の配列のみ。
先生があなたに示したコードは、ポインターが実際に指していると主張するものを指しているという要件に違反しているため、診断が不要な不正なプログラムです(別名「厳密なエイリアス」)。
具体的な例として、コンパイラはプログラムを分析し、A
が直接書き込まれていないこと、およびshort
が書き込まれていないことを確認し、A
が変更されなかったことを証明できます。一度作成された。
B
をいじり回すことはすべて、C++標準の下では、整形式のプログラムではA
を変更できないことが証明できます。
for(;;)
ループまたはrange-forは、A
の静的初期化まで最適化される可能性があります。最適化コンパイラの下での教師のコードは、未定義の動作に最適化されます。
1つの値で初期化された配列を作成する方法が本当に必要な場合は、これを使用できます。
template<std::size_t...Is>
auto index_over(std::index_sequence<Is...>) {
return [](auto&&f)->decltype(auto) {
return f( std::integral_constant<std::size_t, Is>{}... );
};
}
template<std::size_t N>
auto index_upto(std::integral_constant<std::size_t, N> ={})
{
return index_over( std::make_index_sequence<N>{} );
}
template<class T, std::size_t N, T value>
std::array<T, N> make_filled_array() {
return index_upto<N>()( [](auto...Is)->std::array<T,N>{
return {{ (void(Is),value)... }};
});
}
そしていま:
int main() {
auto A = make_filled_array<short, 100, 3>();
std::cout << "\n";
print(A.data(),100);
}
コンパイル時に埋められた配列を作成します。ループは含まれません。
godbolt を使用すると、コンパイル時に配列の値が計算され、50番目の要素にアクセスすると値3が抽出されたことがわかります。
ただし、これは過剰です(および c ++ 14 )。
彼は、複数の配列要素を同時にコピーすることにより、ループの反復回数を削減しようとしていると思います。他のユーザーがここで既に言及したように、このロジックは未定義の動作につながります。
繰り返しの削減がすべての場合、ループの展開により、繰り返しの回数を減らすことができます。しかし、このような小さなアレイではそれほど高速ではありません。
int main() {
short int A[100];
for(int i=0; i<100; i+=4)
{
A[i] = 3;
A[i + 1] = 3;
A[i + 2] = 3;
A[i + 3] = 3;
}
print(A, 100);
}