2020年もすっかり明けて2月になりましたが、年明けに10年ぶりにPCを更新しました。
ちょうど10年ほど前に、購入するPCの世代を統一しようと初代Core i7でソケット1366に決めたのですが、そこからCore i7-980Xを3つ程とi7-920を入手し4台のPCがあるわけですが、その後継ということでZEN2世代のRYZENに決めました。
Core i7を買ったときはちょうどWindows7に乗り換えた時でそこから8,10ときて、ここ2,3年は自分のPCがもっさりしていてグラフィックカードを変えたりしていましたがやっとこさ全とっかえができました。
今回はインテルからAMDに乗り換えたのですが、長いPC歴でちょこちょこAMDを使っています。今までメインマシンで使ったCPUを思い出すだけ書き出すと、こんな感じになります。
1984 (不明)ポケコンPB110
1985 uPD780(Z-80相当品) NEC
1989 80286相当品 AMD
1989 V30 NEC
1992 i486SX(J) Intel
1994 Am486 SX2-66 AMD
1996 Pentium 133 Intel
1997 MMX Pentium 166 Intel
1998 K6 AMD
1998 K6-2 AMD
1998 M2 Cyrix
1999 K6-III AMD
2000 Pentium III 600 Intel
2000 Pentium III 1000 Intel
2002 Celeron 1.4(PentiumIII系) Intel
2003 Celeron 2.3(Northwood-128K) Intel
2003 Pentium4(Northwood) Intel
2004 Athlon 64 3000+ AMD
2006 Pentium D 805 Intel
2006 Core 2 DUO E6400 Intel
2008 Xeon X3350(Core 2 Quad) Intel
2009 Core i7 - 920 Intel
2010 Core i7 - 980X Intel
2020 RYZEN9 3950X AMD
年号は大体ということで割といい加減です。その時の懐事情と趣味とその他諸事情で買い集めたり絞ったりしていましたが、こうしてみると2010年代のスキップぶりが半端ないですね。Core i7についてはSandy Bridge世代でそろえればよかったと少し後悔して、AMDからZenマイクロアーキテクチャが出る噂を聞きつけたときに様子見をしてZen2になったところで「行こう!」となった感じです。
話は戻って、初めての16ビット、32ビット、64ビットCPUは、AMDになります。初めての16ビットパソコンはPC-9801RXでしばらくはIntelを使っていると思っていたのですがあるときに中を開けてみたらAMDのCPUでした。よくよくカタログをみたら80286相当品と書かれていてものすごくがっかりした記憶があります。初めての32ビットCPUは、i486SX(J)と思いきや、このCPUは外部バス16ビットで、それを初めて知った時のがっかり感は半端なかったです。そのあとに買ったパソコンが今はなきコンパックのPresario CDS 524でこちらもメモリの増設で筐体を開けた時にみたらAMDでまたもやがっかりした記憶があります。その後、懐事情が改善し自作に移行して狂ったように買いましたが、初めてのDual-processor, Dual-core, Quad-core, Hexa-core はIntelになります。
RYZEN9は、初めての16-core(書き方を探すのが面倒)、PCI-E Ver4.0(Ver3.0はスキップ)、DDR4-RAM、UEFIです。利用面からは、初めてのCPUプロファイラ(AMDuProf)を使うプロセッサになります。CPUはキャッシュミスとか分岐予測ミスとかが発生すると内部のカウンタで記録をとるのですが、それを読み出すソフトウェアがCPUプロファイラということになります。有名どころではIntelのVTuneがあるのですがこのソフトがめっぽう高くCPUと合わせての購入となると個人では手が出しにくいです。AMDの方はなんと無料ということでまぁAMDということになりました。
そんなものを何に使うのか?と言われそうですが、もちろんADPのインタプリタ部分で、当初はVisualStudio付属のプロファイラを使って最適化を行っていましたが、いろいろ私に合わず、『V-Tuneかー』と思っていたところへ、CodeXL(AMDuProfの前身)の存在を知り、CodeXLに乗り換えたのが5年ほど前になります。CPUがIntelの場合、プロファイラは命令毎にかかった時間が分かるのですが具体的な原因(キャッシュミスなのか?ブランチペナルティか?とか)までは分からずそのあたりは手探りになっておったのがこれでばっちりと分かるようになります。早速プロファイルをしてみると、
パットと見てよくわからない指標があるのでカウンタの意味についてはお勉強が必要なようです。例えばハイライト部分はただの代入になるのですが、それでなぜRet branchとかが関係するのか?(おそらく他のブランチとの関係で結果的に実行された/なかったとか言いたいのかもしれないのですが・・・)とか直接的でないところがあります。
ここにきて、ADPの実行ファイルサイズは約1MBになりますが、今まではプログラムやデータのメモリへの配置はコンパイラに任せていましたがそろそろそういったところまでも手を出す必要があるのかなと思っています。といっても具体的にどうするのか?という話ですが、先ずCPUプロファイラを使いながら基礎データを集めてその上でソースコードを再編集したり、インタプリタ本体を抜き出してミニマムなプログラムを作ってプロファイルをかけたりいろいろ実験ができそうです。
ちなみにこういった話をすると『じゃアセンブラで組めや!』と言われかねないのですが、まぁうざい煽りに真面目に答えると、要は今のプログラムはCPUの潜在能力を十分に生かし切れていないので工夫の余地があり、上手くいけば数倍早いプログラムが作れるということになり、2020年現在ではシングルスレッド性能で数倍といえば時間軸に置き換えると10年以上先に行けるという話になります。
どういうことかと言いますと、例えば1989年に出たi486DX(33MHz)と2000年に出たPentiumIII(1GHz)の性能比は、単純にクロック周波数で見ても30倍(実際はそれ以上)になります。次いで2010年に出たCore i7-980X(3.33GHz、ブースト3.6GHz)とPentiumIII(1GHz)との性能比は、クロック周波数でみて約3.3-3.6倍と伸び率が10分の1程度に減速しています。そして今回のRYZEN9 3950X(3.5GHZブースト4.7GHZ)とCorei7-980Xはクロック周波数ではブースト時で比較して1.3倍、実際に手元にあるADPのプログラムを動かしてみると整数演算で2倍となっています。つまり、それまでは最新のCPUと言えば以前のCPUより格段に速くなって10年も経てば桁違いの速さを見せたのですが2000年代の中盤頃からそのスピードが止まり、今では10年で2倍のパフォーマンスアップに留まることになります。
つまり今まではプアなプログラムを組んでも時間が経てば解決してくれるのですが、これからはきちんと考えて作らないとダメということになります。
CPUプロファイルの話はこの辺にしておいて、今回もう一つ試したいことがあるのが、仮想マシンの活用で今回、私が使う必要のあるプログラムの一部(eTaxとか弥生会計とか)を仮想マシンの方へ移しました。今までは再セットアップとなるとこれらのソフトを再インストールしなければならなくなり面倒なだけなのですが、それが不要となり気軽に再セットアップができるようになるので便利です。欠点としてはOSやらその他のライセンスがインストールするマシンの台数分必要になることと、RYZEN9 3950X特有かもしれませんがCPUプロファイルとの共存ができない(切替にUEFIレベルで設定変更が必要になる)ことでCPUプロファイルを取りたいときはいちいちマシンを再起動することになります。
今月も月末に向かって地味に忙しくなって更新が遅れてしまいました・・・。
前回と前々回に分けてダブルディスパッチについて説明しました。前回の繰り返しになりますが、この考えをさらに進めると複数のオブジェクトに対してポリモーフィズムを効かせればどうなるか?という話になります。一般化ですね。もっとも残念ながら、3つ以上のオブジェクトに対してポリモーフィズムを効かせる具体例が思いつきません。まぁ、『3者面談をやった時の先生と生徒と保護者の性格別の対応策』というのも考えられなくはないですが、今まで3つ以上のオブジェクトに対してポリモーフィズムを働かせた実戦経験はありません。ただ、この一般化-マルチディスパッチとかマルチメソッド-というアイデアを頭の隅に置いておくといつか使える日が来るかもしれません。
n体のオブジェクトに対してポリモーフィズムを働かせる。
と考えた場合、『n=0の時はどうなるか?』という考えが湧いてくるでしょう。そうです。n=0の時はメソッドに関係するオブジェクトが無いと考えられます。従来の関数がそれにあたります。
C++は自然に関数が定義できるのですが、JavaやC#は悪名高いpublic staticメソッドを使って関数を定義します。
以上が前回の復習になりますが、さて『関数が必要となる場合』の具体例をあげましょう。こちらもC++のライブラリからになりますが、一般的に以下のようなアルゴリズムの実装はオブジェクトと関係ないということで関数(正確にはテンプレート関数)で実装されています。
・合計値を求める
・ソートを行う
・検索を行う。
JavaやC#のみ知っている方はこの事実に驚かれるかもしれません。が、アルゴリズム+データ構造=プログラムという古典を知っている方なら逆に納得してもらえるかもしれません。つまり、データ構造に対して独立した手続き-アルゴリズムを考えた場合、関数での実装が上手くいくということになります。
それでもまだピンとこない人がいるかもしれません。そういう方はジェネリックプログラミングについて調べてみるのも良いかもしれません。この連載でジェネリックプログラムを扱うのは話が発散するので今は避けますがJavaにしてもC#にしてもジェネリックプログラミングをサポートしていますが、オブジェクト指向プログラミングとは必ずしも相容れるものではありません。
ということで関数(手続き)は、アルゴリズムの実装と相性がいいという話をしました。他には、実はかのstaticおじさんと揶揄されているみながわさんが指摘した、通常の手続きにも関数が使えるという話をします。
さて、みなさんがあるアプリケーションの実装時に、あるURLで指定されたHTMLファイルを取得する必要があった場合、以下の2つのライブラリのうちどれが便利に使えるでしょうか?
関数 gethtml を使う。
URLクラス、URLConnectionクラス、InputStreamクラス、BufferedReaderクラスを使う。
もし、各種クラスを使う方が便利と思うなら、あなたを『オブジェクト指向おじさん』に認定しましょう。
gethtml関数の方が良いのは自明でしょう。もし疑問があるのならコメント欄に質問してください。
さて、次回は変数の共有との関係で関数とメソッドを比べてみます。
連載も5回目になりましたが、まだモチベーションが維持できています。前回はダブルディスパッチの説明と共にメソッドと関数について本質的な違いはないという説明をしましたが、もう少し突っ込んで話をします。
もともと
object.method();
という表記は、methodがobjectのクラスに属しているという意味合いが強いかと思います。言葉を変えるとクラスとはデータと、そのデータを扱うコードが合わさったものという発想です。これはこれで一つの考え方で構いませんが、問題は全てをメソッドにするのも間違いだということです。その一例がダブルディスパッチということになります。以下のように加算演算子+を考えた場合、
object1 + object2
+演算子は
1.object1に所属するのか?
2.object2に所属するのか?
3.両方か?
4.どちらでもないか?等々
1-4の内どう考えるのが自然なのか?という問題に突き当たります。Javaは演算子のオーバーロードを禁止することによりこの問題から背を向けました。つまりこの問題を避けているとも言えます。ちなみにこのような仕様を見たときにJavaは補助輪が付いた自転車のような印象を受けました。C++は『どちらにも属さない』書き方ができます。正確にいうと『object1に属する』の書き方も場合によってはできますが、例えば、
10 + object
のような場合は、『どちらにも属さない』書き方でしか書けません。つまり関数を使ってオーバーロード演算子の定義を行います。C#はある意味『どちらにも属さない書き方』で書きます。つまり、多くのオブジェクト厨が忌嫌うpublic staticメソッドでオーバーロード演算子の定義を行います。
ダブルディスパッチの話を終える前にマルチディスパッチ(マルチメソッド)の話を軽くします。前回、2つのオブジェクトに対してポリモーフィズムを働かせる仕組みをダブルディスパッチと説明しましたが3つ以上のオブジェクトに対してポリモーフィズムを働かせる考え方もちろんあり、マルチディスパッチまたはマルチメソッドと言います。マルチメソッドをいつ使うのか?と言われたらそれはそれで考え込んでしまうのですが、それでも、メソッドが何処かのクラスに属するという考え方に固執するとこのようなパラダイムを受け入れることは難しいでしょう
さて、このようなある種の一般化を行った後は、今度は全くクラスに属さない手続き=関数という存在も自然に受け入れることができるかと思います。つまり、
y = sin(x);
のような式において、文字通りの関数、sinはどのクラスにも属さないと考えた方がよいでしょう。「Mathクラスに入れたらいいだろう」と反論を受けそうですが、実際にsin関数はJava厨が忌嫌うpublic staticで定義されており、C++でも関数として定義されています。先の演算子の例でも出てきましたが、public staticメソッドとは手続型言語での関数定義に相当するものになります。ちなみにかのεπιστημηさんは
私との議論で、
Java/C#はどのクラスにも属さないメソッドが作れないため、
当たり障りのないクラス(たとえばMathあるいはModuleC)をでっちあげてそいつに押し込まざるを得ない。
「いかなるクラスにも属さない」という"思い"を表現できません。
僕にはそこが(C++と比べて)不自由に感じます。
というふうに本質的に関数であるものを仮のクラス(Math)に入れることには難色を示されています、これについては私も同感です。
さて、『そんなに関数が重要ならsinとかではなくもっと幅広く使える例を出せ!』と言われそうです。その答えは来週に行うということで。
なんやかんやでかろうじて連載も4回目になりまして、モチベーションも維持できてよかったです。語学留学もあと一カ月で、最近、仲良くなった大学生にC++を教えています。といっても最初はポインタを教えていたのですが次にリファレンスを教えることになって、思わず、「混乱の元だからリファレンスは最初は無理して覚えなくてよくてポインタを先に覚えましょう。」と言ってしまった。もっとも「リファレンスの方が簡単だから軽く覚えてガッツリポインタを勉強しよう。」とも言ったが、この連載のターゲットは『オブジェクト指向しか知らない世代』としていますが、JavaやC#しか知らない方たちはポインタの概念が理解できているか怪しい限りです。興味深いのは、私の半径3メートルの範囲、クパチーノ近辺ではC++をマスターすることがステータスになっているようです。
という訳(?)で今回はメソッドと関数について話します。なぜか知らぬがネットの界隈ではどうも、
メソッド>>>どうしようもない壁>>>関数
という図式が成り立っているようです。でこのどうしようもない壁なんて無いというのが今回のテーマです。もっとも1回で説明するのは難しいので何回かに分けて説明します(しかも飛び飛びになるかも)。
先ず、最初に以下の2つの表現
object.function();
function(object);
ですが、C++でみると生成される機械語コードは同じになります。例外はfunctionが仮想関数(ややこしいので以下、オーバーライドメソッドと呼びます)の場合になります。オーバーライドメソッドを除いて、C++でみると上記の2つのコードは同じということになります。『カプセル化があるだろ』という突っ込みがあるかもしれませんが、それは他のキーワード(friend)を使うことによって同等にできます。
もし、あなたには『どうしようもない壁』が見えるというのならどうぞ具体例とともにコメントをください。
ということで、ADPでは上記の2つのコードは同じように解釈されます。というか上のコードは下のコードとして解釈されます。『同じなら一つの書き方で良いだろ!』とお叱りを受けそうですが、表記上の重要な違いがあります。つまりobject.function()の記述はメソッドチェーンを実現できます。
object.function1().function2().function3();
もしこれを関数の形式で書くと
function3(function2(function1(object)));
となりますが、どちらが良いかは一目瞭然だと思います。ただ、これはあくまでも表記上の話でもしこれがどうしようもない壁というのならオブジェクト指向というのは表記上の問題ということで話が付きますし、そもそもメソッドチェーンの見た目はもはやメッセージパラダイムとは異なるでしょう。
さてfunctionがオーバーライドメソッドの場合の話ですが、簡単にオーバーライドメソッドについて話ますと、呼び出されるメソッドが実行時に決定される点です。C言語の関数は呼び出される実体がコンパイル時に決まりますが、オーバーライドメソッドの場合は実行時にオブジェクトの型によって呼び出される実体が決まります。
object.add(1);
とあった場合、objectがInteger型ならIntegerクラスのaddが呼び出され、objectがFloat型ならFloatクラスのaddが呼び出されるということになります。これはポリモーフィズムと呼ばれるもので、オブジェクト指向病にかかっている人たちはまさにポリモーフィズムが手続き型言語との差別化を図っていると信じています。ポリモーフィズムについては連載の後の方で詳しく説明しますが、関数ポインタを使うとこによりCやアセンブリ言語でも同様の機能を実現できます。こう言うと『ならアセンブリ言語で全部書けや!』と意味不明な切り返しをされるのですが、それに答ますとLinuxの記述では主にCが使われているというのは有名な話ですが、でデバイスドライバの実装等ポリモーフィズムと同様のことが行われています。つまりCでもある程度はオブジェクト指向を実用レベルでシミュレートできるということです。ちなみにADPはC++で記述しておりポリモーフィズムもバリバリ使っていますがパフォーマンス上の理由からCでリライトしようかと思案中です。
さて、話が横道にそれましたので元に戻しますとaddの例ですが、何か変なことに気付かないでしょうか?例えば以下の例はどうでしょうか?
object1.add(object2); // ・・・・・(A)
このaddメソッドですが、object1に対してはポリモーフィズムが効きますがobject2に対してはどうでしょうか?JavaやC++ではobject2にはポリモーフィズムは働きません。まぁ関数の例でもそうでしたが引数にはポリモーフィズムは働かないと考えてもしようがない面もありますが、もし引数にポリモーフィズムが働かないのが当然だというのなら(A)は以下の例と動作が異なることになります。
object2.add(object1); // ・・・・・(B)
加算(add)というのは通常交換法則が成り立ちますのでもし(A)と(B)の動作が異なるということならその実装は不完全としか言いようがないですね。ので通常addがポリモーフィズムであるならそれはobject1,object2双方に対してポリモーフィズムでなければならないことになります。2つのオブジェクトに対してポリモーフィズムを働かせることを
ダブルディスパッチと呼びます。私はダブルディスパッチについて16年程前に『More Effective C++』で知ったのですが、以前、といっても2,3年程前にJavaが得意で自称オブジェクト指向をマスターしている彼が『オブジェクト指向をマスターしている人は日本にはほとんどいない。俺を除いて』と言っていたので『ダブルディスパッチって知っている?』と聞いたら『なにそれ?』と返されたことがある。ということで知ったかのJava厨の検出にはダブルディスパッチは今のところ効果的かと思われる。ちなみにC#4.0以降の場合、dynamicキーワードを使うことにより(A)の記述でもobject2に対してもポリモーフィズムを働かせることができるのでC#プログラマにダブルディスパッチの技を繰り出すのは返り討ちにあう可能性があるので注意したい。話を戻してC++やJavaの場合はかの有名なデザインパターンの一つビジターパターンがダブルディスパッチの実装方法の一つとなる。
ダブルディスパッチの使いどころですが、二項演算子でポリモーフィズムを使いたいとき(もっともパフォーマンス上の問題があり二項演算子でポリモーフィズムは使わない場合が多い)や2つの物体の衝突を計算する場合(More Effective C++)があるでしょう。ADPではユニフィケーションの実装にビジターパターンによるダブルディスパッチを使用しています(これがしたいが為にC++を使ったがもっと効率的な記述をしたいからCで組みなおそうかと考えている)。
つまりこういうことになる。
object.method()
と
function(object)
において表現上の違いやプログラミング言語のサポート状況による違いはあるが本質的には両者を同等物とみて構わないということである。つまりどうしようもない壁はないということになる。
長くなったので続きは来週に持ち越します。
前回の記事ですが、読者の方から洗脳を解くには弱いという指摘を受けたのでちょっと補足を考えてみました。実際にオブジェクト指向に洗脳されている方には何をいっても聞かないのですが、そもそも論として、なんでこんな記事を書くのか?ということを説明しましょう。
一般的な話になりますが、エンジニアの中には原理主義者よろしく自身の考えを曲げない人が確かにいます。その人の世界で頑張ってもらえればよいのですが原理主義者の中には妥協という言葉をしらないのか、他人のコードや考え方に口出しをする方もいらっしゃいます。たとえば『手続型はダメ。オブジェクト指向はよい』とかですね。彼・彼女が完成されたエンジニアなら口出しも立派なアドバイスとして成立するでしょうが、ある意味究極のセオリーだと私が考えるのは『コードに正解はない』ということです。あるとすれば『こちらの方がより適切かもしれない』ということと『動くコードはやっぱり最強』ということです。あるエンジニアに、その昔私が書いたコードをありがたく手術と称してリファクタリングしてもらいましたがテストをすると重要なエラーチェックが抜けていることに気づきました。質問すると「まだ書いていない」という返事をもらいました。つまりこういうことです。エラーチェックのコードは時として見栄えが悪くなります。まぁ通常処理に加えて異常系の処理を加える関係上仕方がないでしょう。そういうコードから異常系の処理を取り払えば確かに見栄えはよくなります。さて、見栄えは良いが重要な処理が抜けているコードと見栄えは悪いがきちんと処理が入っているコード、あなたはどちらを評価するでしょうか?見栄えを取るという人は将来失業しないように気を付けましょう。
オブジェクト指向に心酔するのは構わないのですが、結論から言いますと残念ながらオブジェクト指向技術は思ったほど役には立ちません。詳しくは連載を通して理解してもらえればよいのですが、非常に残念なことにオブジェクト指向に過剰に心酔する人がいるのも事実です。例えば、この記事です。
staticおじさん達に伝えたい、手続き指向とオブジェクト指向の再利用の考え方の違いについて
経験のあるエンジニアならこの記事が眉唾だと理解できるのですが経験の浅いエンジニアなら真に受けてしまうでしょう。少し技術的な点を突っ込んでおきます。記事中
アセンブリやCOBOLのような言語では、オブジェクト指向言語で一般的なカプセル化という考え方がきわめて弱いということがあります。変数は静的なグローバル変数が中心であり、
とありますが、アセンブリ言語でもローカル変数はあります。またいわゆるメンバ変数もシミュレートできます。もし仮にアセンブリでローカル変数がなかったりメンバ変数をシミュレートできないとすればJavaやC++のコードはどうやって実行されると考えているのでしょうか?OSはどんなふうに記述されていると考えているのでしょうか?JavaやC++はコンパイラを通して一旦機械語に変換されます。アセンブリとはその機械語に一対一に対応します。また、そもそも論としてアセンブリとCOBOLを同じテーブルに並べるところにもの凄い違和感を感じます。もしあなたが違和感を感じられないのならオブジェクト指向を勉強する前に他に勉強することがあるということになります。
さてこの方の約3年後の他の記事を見てみましょう。
開発チームにアーキテクトがいないなと感じてしまうような、残念なコードスメルの例
引用しますと、
最後に、最近になって気づいた自分の間違えについて書いておかなくてはなりませんね。以前であればこうした設計上の問題は日本のSI業界の構造が問題なのであるという話をしていたかもしれません(^_^;)が、ここで書いたような話は多少フィクションが入っているとはいえ、実際私がSI業界以外の今の会社で体験したことに基づいていると告白しなくてはなりません。言語の特性から、Javaで開発していると、こういった設計上の問題が起きやすいということがある可能性もありますが、こういった話はSIer以外でも、どこの国の開発チームでもあるのだなということですね。
この中にある
Javaで開発していると、こういった設計上の問題が起きやすいということがある可能性もあります
この感覚は実はオブジェクト指向症候群から脱する第一歩になります。多くの生き残っているエンジニアは大なり小なりこういう感覚を端緒とし、次に”Javaはダメだ○○は良い”となり、最後には”オブジェクト指向がダメなんだ”となります。もちろんですが私も20年程前にこういう感覚に襲われたことがあります。
まとめますと、オブジェクト指向症候群にかかると間違った知識をまき散らします。さらに病気から抜け出すと後でそれを修正しなければならないという二度手間が発生するわけです。できれば早く抜け出した方が本人の為でもありますし業界のためでもあります。
と長くなったのと月末でいろいろ忙しいので、今週はこれにて終了です。次回は、メソッドと関数についてもう少し突っ込んで話したいと思います。