とても勉強になったので写経のつもりで翻訳しました。
普段自分でもわかっている気がするけど言語化するのが難しかったこと、自分でやっていて確かにあとから痛い目を見たかもしれないと反省を促される内容でした。
※ゲームプログラミング特有だなと感じた内容はきちんと理解できなかったので割愛しています。
Now that people have already said highly controversial stuff like ”debugger is useless for C++ development”, I think I can share my own controversial thoughts about unit testing, DRY, copy-paste coding and function length, etc... with 20 years of C++ programming experience.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
他の人が「C++開発においてデバッガは使い物にならない」みたいな物議を醸しそうな発言をしてくれたし僕も20年来のC++プログラミングの経験を通して学んだ単体テスト、DRY、コピペ指向プログラミング、関数の長さ、その他諸々について思ってることを共有できそうな気がする。
I want to start this rant by telling that I have made (or allowed my team to make) countless of programming mistakes during my career: Textbook OOP (bird is animal), template monsters, too much codegen, over-engineered solutions, overly generic code, overuse of raw sync prims...
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
まずはじめに僕はたくさんの間違いをして(あるいはチームにするように仕向けて)きたことを伝えておきたい:教科書通りのOOP(鳥は動物である)、テンプレートの怪物、コード生成のやりすぎ、オーバーエンジニアリング、過度に抽象的なコード、生同期プリミティブの使いすぎ...
Each unit test is an additional dependency. Another call site that uses your function/class/data. Adding a dependency to code/data that has zero dependencies is not free. It adds inertia. Further changes of that code slow down and some refactorings/optimizations become infeasible
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
単体テストは一つの依存だ。関数・クラス・データを使うもう一つの呼び出し元。依存ゼロのコード・データに依存を追加することはタダじゃない。重くなる。そのコードへの変更は遅くなるしいくつかのリファクタリング・最適化はできなくなる。
Adding an extra dependency (test case) for common library code (math, containers, etc) doesn’t add intertia, since code like this already has lots of dependencies and well defined API that doesn’t change. Test cases bring high benefit and very little downsides for code like this.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
よくあるライブラリ系のコード(算数、コンテナなど)はもともとたくさんの依存があって変わらないよく定義されたAPIがあるから依存を追加すること(テストケース)によって重くなることはない。こういうコードのテストはとても有益でほとんど害がない。
A good practice is to copy-paste code three times, and then refactor (extract) if all three instances are still doing the same thing. Before this, you don’t want to add unit tests, because your code has no dependencies. Code without dependencies is the best code. Safe to modify.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
いい慣例として三回同じコードをコピペしてそれでも同じ振る舞いをしているならリファクタリング(抽出)するといい。そこに至るまではコードに依存がないから単体テストに追加しないほうがいい。依存のないコードが一番いいコードだ。安全に改変できる。
#1 problem in big code bases is entangled code dependencies. If you make code too generic or extract code too soon, you end up with more dependencies. Code used by 2+ call sites tends to eventually bloat with complex control flow. Bad performance, hard to understand and modify.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
大きいコードベースで一番大きな問題は複雑に絡み合ったコードの依存関係だ。早まった抽象化や抜き出しは依存関係を増やす。二箇所以上から呼び出されるコードはいずれ複雑な制御フローで溢れてしまう傾向にある。パフォーマンスは悪く、わかりにくくて改変が難しい。
There are two types of long functions. Entangled and linear. If you avoid dependencies (see above), you can often write long functions which progress perfectly linearly. Trivial to read, modify and understand. No jumping around the code base. Minimal brain load.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
世の中には二種類の長い関数がある。もつれたものとそうでないもの。前述のように依存を作ることを避ければ流れるように進行する長い関数を書くことができる。読みやすく、わかりやすく、改変しやすい。コードベース内を飛び回ることがない。脳にかかる負荷が小さい。
It might seem like a good idea at first to split several such long functions to small functions and reuse some pieces of shared code. But this adds dependency between these functions and reading experience is no longer linear. Modifications affect many functions -> higher risk.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
一見そういう長い関数は小さな関数として区切って共通するコードを使いまわしたほうがいいように見えるかもしれない。でもそうすることでそれらの関数の間の依存関係は増え、直線的にコードを読むことができなくなる。改変は多くの関数に影響を与えるからリスクが高い。
Programs transform data. Data dependencies are the actual dependencies and define which data you need to process in which order, how you can parallelize the processing and which invariants need to be maintained. Code dependencies are often false dependencies.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
プログラムはデータを変換する。データの依存関係こそが真の依存関係であり、どのデータをどの順番で処理して、どうやって処理を並列化して、どの普遍性を維持しなければならないかを規定する。コードの依存関係は多くの場合見せかけの依存関係にすぎない。
As said above, large functions aren’t usually a problem. The same is not true for large classes/structs containing lots of data unrelated to each other (Baseobject syndrome). You should never use real world object abstractions when you decide where you put each piece of data...
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
前述の通り多くの場合関数が大きすぎることが問題にはならない。巨大なクラスや構造体が互いに関係のない大量のデータを含んでいる場合はその限りではないけれど(Baseobject症候群)。どこにどのデータを置くか決めるときに実世界の「モノ」の抽象化は絶対に使わないほうがいいよ...
Instead, you should split your data according to all transforms you performed to that data. Data commonly accessed together go together. Fields not accessed at most call sites get split to separate structures. This avoids false dependencies and improves data cache utilization.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
代わりにそのデータに対して適用するすべての変換を元にデータを分けるといい。頻繁に一緒にアクセスされるデータは一緒にいるべきだし、ほとんどの呼び出し元で使われない属性は別の構造に切り分ける。こうすることで見せかけの依存関係はできなくなるしキャッシュの利用効率がよくなる。
Critical section (mutex, etc) teaches a wrong way to think about synchronization. You don’t want to synchronize code. Code is immutable. Race conditions (RAW, WAR, WAW) are all pure data hazards. Shared sync primitive guarding one function/class is often a code smell.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
クリティカルセクション(排他制御など)は同期について間違ったことを教えてしまう。コードを同期したいことはないはずだ。コードは不変だ。競合状態はすべて純粋にデータの問題だ。一つの関数・クラスを守る同期プリミティブが共有されていたらそれは臭いことが多い。
Instead of fine grained sync primitives, you want to split your data so that you remove false dependencies. This often allows trivial parallel work and at the same time makes your code base much easier to modify and maintain. And improves your performance too (cache utilization).
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
細かい同期プリミティブを使わずに見せかけの依存関係をへらすようにデータを分割したほうがいい。こうすることで単純な並列化が可能になるし同時にコードベースを維持・管理するのが簡単になる。キャッシュによるパフォーマンス改善にもなる。
You can’t multithread your code if you can’t ensure no data races. Processing of objects filled with pointers and references to other objects are hard to parallelize. Virtual functions are even worse. How can you know which data is accessed, if the call target is not known?
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
データの競合状態がないことを保証できなければマルチスレッド化なんてできない。他のオブジェクトへのポインタや参照をたくさん持ったオブジェクトは非常に並列化しにくい。仮想関数に至ってさらにひどい。呼び出しのターゲットがわからないのにどのデータがアクセスされるかなんてわかるわけがない。
Most of your perf bottlenecks are in loops. No function is slow if you don’t call it many times. If you fully understand all data used inside a loop, you can often use parallel loop to scale it to all cores. Abstractions, virtual funcs and data dependencies make this hard.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
パフォーマンスのボトルネックはほとんどループの中にある。大量に呼ばない限りは遅い関数なんて存在しない。ループの中で使われているデータをきちんと理解していれば並列化によってコア数分までスケールさせられる。抽象化、仮想関数、そしてデータの依存はこれを難しくする。
Parallel for loop is one of the safest multithreaded optimizations you can make if you use simple data structures with no hidden data. Parallel for loop can be self contained inside a function. No need to modify other code. Works well with task/job schedulers too.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
単純なデータ構造を使ってさえいればループの並列化は最も安全な最適化だ。並列forループは関数の中に閉じ込めることができる。他のコードを改変する必要なんてない。タスク・ジョブスケジューラとも相性がいい。
”Primature optimization is root of all evil” is the most misunderstood sentence in CS. Planning your data structures is part of the architecture design, not an optimization. Good data layout both improves performance dramatically, and also improves code maintainability.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
「早まった最適化は諸悪の根源である」は計算機科学においてもっとも誤解された文だ。データ構造の計画はアーキテクチャ設計の一部であって最適化ではない。いいデータレイアウトはパフォーマンスを劇的に向上させるだけでなくコードの管理を楽にする。
Better version ”Do not micro-optimize unless profiler shows a bottleneck in that code”. Use profiling tools from the beginning of the project to react to problems before it is too late. Profile often and automate profiling for QA.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
言い換えるなら「プロファイラがボトルネックを発見するまで細かい最適化をするな」。プロファイリングツールは早いうちから使ってなるべく早めに問題を発見できるようにしよう。頻繁にプロファイルをとって自動プロファイリングで品質保証を。
Best way to make future proof code: Make simplest possible code meeting current requirements. Minimize code/data dependencies. Overly generic code is not future proof. It handles some extra cases, but is overly complex. More refactoring to make it do what you actually need next.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
未来に備えたコードを作る一番の方法:今の要求を満たす最小限のコードを作ること。コード・データの依存関係を最小化する。過剰に抽象的なコードがいいとは限らない。いくつか特殊ケースに対応してくれるかもしれないけど過剰に複雑だ。次に必要なことを実現するためにリファクタリングをしよう。
Predicting future is hard. Try delaying decisions and writing code as late as possible. This way you make decisions and write code that better suits the actual needs. Coding a feature that nobody uses is waste of time. Coding a feature too early often leads to big refactoring.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
未来を予想するのは難しい。決断を下しコードを書くことをなるべく遅らせよう。こうすることで実際のニーズに合った判断をしてコードを書くことができる。誰も使わない機能を書くのは時間の無駄だ。早まった機能の実装は大きなリファクタリングに繋がりやすい。
Unknown object life time or owner is a code smell (ref count, shared_ptr). In most cases you should be able to define a clear owner for each object. In multithreaded environment custom destructors are very dangerous with ref count schemes. Hard to know when destructor is called.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
オブジェクトのライフタイムや所有権が未知のコードは臭い(レファレンスカウント、共有ポインタ)。ほとんどの場合あらゆるオブジェクトの明確な所有者を定義できるはずだ。マルチスレッド環境ではカスタムのデストラクタとレファレンスカウントは致命的に相性が悪い。デストラクタがいつ呼ばれるのかがわかりづらい。
I don’t personally hate unique_ptr or other non-refcounted RAII. But beware of memory allocation cost, mem fragmentation (on consoles and mobile) and pointer indirection cost (cache miss). Prefer value members instead of separate alloc for a member and use custom allocators...
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
ユニークポインタを含むその他レファレンスカウントされないRAIIは僕も嫌いじゃない。でもメモリアロケーションにかかるコスト、メモリのフラグメンテーション(コンソールやモバイルなど)、そして間接参照のコスト(キャッシュミス)を忘れないこと。カスタムアロケータを使って別途メンバをアロケートするのではなく値を持ったメンバを使うことを好もう。
The best way to allocate objects of certain type is to allocate a big block of storage for all of them and put them next to each other. Linear memory access pattern is much more cache friendly than allocating separate memory for each object. Big performance difference.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
特定の型を持つオブジェクトをアロケートする一番の方法は大きなストレージ領域を確保しておいて全部隣通しに置くことだ。線形なメモリアクセスパターンはバラバラにアロケートするよりキャッシュフレンドリーだ。パフォーマンスに大きな差が出る。
If you use manual new/delete, use a memory leak detection tool. It’s also simple to wrap global new/delete to create your own leak tracker. Keep leak tracker active in daily dev builds. Once you have a custom mem tracker, you can use it to dump memory stats too. Very useful.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月1日
手動new/deleteを使っている場合はメモリリーク検出ツールを使おう。グローバルなnew/deleteをラップすることで簡単にリーク追跡もできる。日頃の開発ビルドではリーク追跡を有効にしよう。メモリトラッカーを使い始めたらメモリの情報もダンプしよう。とても便利。
When refactoring code, make a local branch (git) or shelve (P4). This makes it easier to ”throw away” your changes if you don’t like the result. Code will still be available for later use if you want to revisit. Don’t push refactorings to dev main unless you are 100% happy.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月2日
リファクタリングするときはローカルブランチ(git)もしくはシェルフ(P4)を作ろう。こうすると結果が良くなかったときに変更を簡単に捨てることができるし、あとでまた触りたくなったらまだコードが残ってる。変更に100%満足していない限りは主要ブランチにマージはしないほうがいい。
Every refactoring attempt gives you information. Never consider a failed attempt as useless work. However, focus your refactoring efforts to code that is actively in development. Code that works fine and doesn’t need any near future change doesn’t need to be touched.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月2日
すべてのリファクタリングの試みは情報をくれる。失敗したからと言って無駄にはならない。でも開発が活発なコードにリファクタリングの労力を注ごう。きちんと動いていて近い将来に変更が入る予定のないコードを触る必要はない。
Write well performing code by default, but don’t micro-optimize without profiling. When optimizing code, benchmark result on all target platforms and don’t push to dev main unless result is 100% clear (no regressions) and you are happy about code quality. Branch/shelve if unclear
— Sebastian Aaltonen (@SebAaltonen) 2019年1月2日
パフォーマンスがいいコードを書きつつ、プロファイリングをせずに細かい最適化を施さないようにしよう。最適化をするときはすべてのターゲットプラットフォームでベンチマークをとって結果が100%明瞭でコードの品質に満足している場合のみ主要ブランチにマージしよう。そうでなければブランチを切る。
Store profile trace/capture files for all platforms to maintain history. Name files after latest optimization/change. Do before/after compare after every optimization on all platforms. Write email + brief info to commit about gains. Automate regression tests for QA.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月2日
履歴を残すためにすべてのプラットフォームのプロファイルのトレース・キャプチャファイルを保持しよう。最新の最適化・変更を元にファイルに名前をつけよう。最適化の前後ですべてのプラットフォームでビフォー・アフターを比較しよう。得たゲインについてメールやメモを残そう。品質保証のためにリグレッションテストを自動化しよう。
Pure ALU instruction count is not the most common performance bottleneck. Use platform specific low level profiling tools to find out the actual bottleneck instead of wasting time doing wrong optimizations. In CPU code, memory latency and cache are the most common bottlenecks.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月2日
純粋なALU命令カウントはパフォーマンスボトルネックではないことが多い。間違った最適化のために時間を無駄にするのではなくプラットフォーム専用の低レベルプロファイリングツールを使って本当のボトルネックを探そう。CPUコードではメモリレイテンシとキャッシュが最もよくあるボトルネックだ。
Memory load->addr->load dependency chains (such as linked lists) are #1 poison for modern out-of-order CPUs. Prefer breadth first over depth first tree traversal to expose more parallelism for the CPU. CPU starts loading memory of all siblings concurrently, amortizing latency.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月2日
現代のアウトオブオーダーCPUにとって最も毒なのはメモリload→addr→loadの依存チェーン(連結リストなど)だ1。CPUの並列化を促すために深さ優先ではなく幅優先探索を使おう。CPUがすべての小要素のメモリを同時に読み込むからレイテンシを帳消しにできる。
Before adopting a new C++ standard ensure that all target platform compilers have robust support. Investigate codegen and compile times for every feature you want to use on all compilers. It’s OK to ban certain feature if you feel it’s too risky to adapt at current state.
— Sebastian Aaltonen (@SebAaltonen) 2019年1月2日
新しいC++の標準を採用する前にすべてのターゲットプラットフォームのコンパイラがサポートしていることを確認しよう。すべてのコンパイラで使いたい機能のコード生成やコンパイル時間を調べておこう。現状では使うのがリスキーだと思う機能は使用を禁止してもいい。
-
ここの部分はきちんとわかっていません。ご指摘をいただけると幸いです。 ↩