プログラムを高速化したいときは、時間計測結果を元に戦略を立てるのが前提であるべきだと思うけど、傾向としてループ部分がクリティカルな状況はよくある。
1回だけ呼ばれる処理を100ms高速化するより、
100万回呼ばれる処理を0.01ms高速化する方が効果が大きくてハードルが低い(ことが多い)
平たく言えばこういうのをどう高速化するかの話。
for(auto &&obj: objects) {
obj.update();
}
実際にこう書くと速くなるというよりは高速化手法のバリエーションの話だと思って欲しい。
これ意味ないとか逆効果とかの指摘歓迎します。
ループ内を高速化するアプローチ
Object::update
内を速くするのが大前提なんだけど、一般的な議論は難しいのでよくある状況を書いとく。
フラグチェックを外に出す
もしこんなコードがあったら
void Object::update() {
if(this->shouldUpdate()) {
// updateの内容
}
}
for(auto &&obj: objects) {
obj.update();
}
これの方が無駄な関数呼び出しがない分速い(ことが多い)
void Object::update() {
// updateの内容
}
for(auto &&obj: objects) {
if(obj.shouldUpdate()) {
obj.update();
}
}
加えてshouldUpdate
関数をinline
指定しておくと速い(ことがある)
inline bool Object::shouldUpdate() {
return is_awake;
}
実体を使う
下記は前者よりも後者の方が、ポインタのデリファレンス分有利。
実体で良いならそもそもポインタを使う設計にしてないと思うので、使う場面はほぼないとは思う。
std::vector<Object*> objects;
for(auto &&obj: objects) {
obj->update();
}
std::vector<Object> objects;
for(auto &&obj: objects) {
obj.update();
}
ループ対象を厳選するアプローチ
shouldUpdate
みたいなフラグがあるとしたら、それがfalse
のやつはループから除外すれば良い。
- フラグチェックのコストが高い
- sleep状態(updateを呼ぶ必要がない)にして良いオブジェクト数が多い
- awakeとsleepの切り替わりが頻繁でない
など、フラグチェックよりもコンテナへの出し入れの方がコストが安い場合は有効(なことが多い)
ObjectArray awakeObjects, sleepObjects;
void update() {
for(auto &&obj: awakeObjects) {
obj.update();
}
}
void sleep(Object &obj) {
awakeObjects.remove(obj);
sleepObjects.add(obj);
}
void awake(Object &obj) {
awakeObjects.add(obj);
sleepObjects.remove(obj);
}
ループ自体にかかるコストを減らすアプローチ
ループ内のコストが十分安いと、ループ自体にかかるコストが無視できなくなってくる。
例えばよくある
std::vector<Object> objects;
for(int i = 0; i < objects.size(); i++) {
objects[i].update();
}
みたいなコードだとObject::update()
の他に
-
vec.size()
の呼び出し -
i
とvec.size()
の戻り値との比較 -
i
のインクリメント - vector要素へのランダムアクセス
がループ回数分呼ばれるので、このあたりに無駄がないか考えてみると良い。
コンテナを選ぶ
[C++] STLの型の使い分け
この記事でほぼ話は終わってるので読むと良いと思います。
イテレーションの速度に関してだけいうなら、vector
かdeque
を使っとくと良いと思う。
一度だけで良い処理はループから出す
これより
for(std::size_t i = 0; i < objects.size(); ++i) {}
for(auto it = std::begin(objects); it != std::end(objects); ++it) {}
これの方が速い(ことが多い)
for(std::size_t i = 0, end = objects.size(); i < end ++i) {}
for(auto it = std::begin(objects), end = std::end(objects); it != end; ++it) {}
よくあるこれも
for(int y = 0; y < image.getHeight(); ++y) {
for(int x = 0; x < image.getWidth(); ++x) {
}
}
こうすると速くなる(ことが多い)
int w = image.getWidth();
int h = image.getHeight();
for(int y = 0; y < h; ++y) {
for(int x = 0; x < w; ++x) {
}
}
ループ回数を減らすアプローチ
これは半分冗談だけど、発想としてはあっても良いと思う。
ループ内をひらく
for(std::size_t i = 0, end = objects.size(); i < num; i += 4) {
objects[i].update();
objects[i+1].update();
objects[i+2].update();
objects[i+3].update();
}
ループ頻度を減らす
static std::size_t counter = 0;
const std::size_t skip_length = 4;
for(std::size_t i = counter%skip_length, end = objects.size(); i < end; i += skip_length) {
object[i].update();
}
++counter;
再掲
これ意味ないとか逆効果とかの指摘歓迎します。