kbcl0のページ#1
(4) 開発日記#1
- 2019.04.27(土)
- [1] とりあえずC++に切り替えたらどこまでできるかに挑戦。C++にはコンストラクタがあって、初期化のための関数呼び出しを書く必要がないので、これは行数を節約できそう。さらにデストラクタもあるので、解放処理も書かなくていい。おおこれはすごい。やる前から理屈ではわかっていたけど、いざ実際にオブジェクトを宣言しただけでコンストラクタが確実に呼ばれて、変数スコープから抜けるだけでデストラクタが呼ばれるので、なんかびっくりするほど簡単に感じる。・・・そうかー、C++が大好きでいつも使っている人はこんな世界で暮らしていたのかー。すごく甘やかされている感じがするけど、確かにこれは慣れたらやめられないかも・・・。
- [2] kclib1のときは「初期化忘れがないかチェックするコードを入れるかどうかで、デバッグモードとリリースモードを用意していたのだけど、もう初期化忘れは基本的に想定しなくてよくなったので、チェックするコードはいらなくなって、シンプルになった。
- 2019.04.28(日)
- [1] このkbcl0では「積み上げ」型の開発を指向しているわけだけど、そうするとクラスの依存関係は当然かなり出てくる。例外なく、先に作ったものから順番に準備していけば破綻はない(=開発するときに未来の開発物を必要とするような書き方はしていない)のだけど、しかしそれでもグローバルスコープにオブジェクトを置こうすると、C++ではグローバルスコープのオブジェクトのコンストラクタの呼び出し順序に関する保証が何もないので、準備しておいてほしい下位のものがまだできていないのに上位のコンストラクタが呼び出されるという、ややこしいことが発生した。
- これを回避するために以下の処置をとった。
- (1)まずクラスを継承してstartup対応クラスを作る。
- (2)このstartup対応クラスでは、コンストラクタが複数回呼ばれるかもしれないことを想定し、最初の一回だけ親クラスのコンストラクタを呼び出すようにする。・・・startup対応クラスを作る必要があるのは、コンストラクタを上記のように書き換えることに対応するためである。・・・startup対応クラスは例外なくstatic領域に置かれるので、起動時にすべてのフィールドが0になっていることが保証される。したがって最初の呼び出しかどうかを確認するのはとてもやさしい。
- (3)さらにstartup対応クラスのコンストラクタでは、自分より下位の依存関係にあるオブジェクトが初期化済みかどうかを確認する処理を入れる。もし初期化が終わっていないようなら、replacement newの構文を使って下位のオブジェクトのコンストラクタを呼び出してやる。
- (4)もちろんこれをデフォルトの動作にしてしまえば、わざわざstartup対応クラスなど作る必要はなくなるが、しかしそうすると毎回コンストラクタの処理に時間がかかるようになる。しかもヒープやスタック上のオブジェクトは初期化前にすべてのフィールドが0になっているという保証がないため、初期化済みかどうかを判定するのが難しく、簡単な方法で処理した場合、運が悪いと一度も初期化できなくなるという可能性がある。だからstartup対応クラスを作るのが一番手っ取り早い。
- これはめんどくさいなーと感じたけど、でもまあC言語に戻りたいと思うほどではない。いやいや全然そんな風には思わない!
- [2] テキストファイルを読み込んで、行単位で逆順で表示するだけのプログラム。
#include "kbcl0.h"
#include <stdio.h>
int main()
{
char *s = (char *) kreadFileA("kbcl0.h", "rb", 1 + 2);
KSizPtr sp;
while (*s != '\0')
sp.addPtr(kcutCrLfM(ksgetsA(&s)));
for (int i = sp.s / sizeof (char *) - 1; i >= 0; i--)
puts((char *) sp.getPtr(i));
return 0;
}
- これは結構シンプル!初期化を明示的に書かなくてもよくなったことが大きい。
- 2019.05.09(木)
- [1] g++を6.3.0にアップデートした。おおこいつは速いなー。コンパイルスピードは比較してないけど、でも生成された実行ファイルの実行速度はかなり速くなっている。すばらしい。とりあえず今後しばらくはこっちを使おう。
- そもそもg++をアップデートしようと思ったきっかけは、inline指定に関するg++のバグを踏んでしまい、「うーん、最新版にしたらこのバグは解消するのかな」と思ってやってみたら、見事に直っただけではなく速くなったということ。これはめでたい。
- [2] データをどんどんと追記していきたいというケースはよくある。この場合、片方向リストでどんどんつないでいく方法と、KSizPtrような可変長配列を使って追記していく方法があると思う。
- 可変長配列の場合、サイズが大きくなってくるとreallocの際にmemcpyをしなければいけなくなって、それで遅くなる。一方で片方向リストでつないでいく場合だと、追加の際にmemcpyが発生することはないが、ポインタの分だけメモリ消費量が増えるし、たどっていくアクセスが単純な配列と比べたらどうしても遅くなってしまうという問題がある。
- 直感的には、追記していくデータのサイズが4バイト程度なら、おそらく可変長配列のほうが速い。なぜなら片方向リストにしたら、追加されるデータが2倍になってしまうから(ポインタの分だけ増える)。可変長配列ならたまにmemcpyは発生するが、きっとそれでも可変長配列の方が速いだろう。・・・一方で、追加するデータの単位が100バイトとかになってくると、memcpyのコストがかなり高くなってくるので、片方向リストの方が速そうだ。
- ここまでは直感的にわかるのだけど、ではこの逆転が起きるのは何バイトくらいからなのだろう。それを知っておくことは今後のプログラミングにおいて有益なはずだ。ということで、簡単なプログラムを書いて測定してみた。・・・プログラムは、データを淡々と100万回追加していき、それができたらこれを一度一番最初から終わりまでリードアクセスしてみて、そしてこのデータをすべて破棄する。これを1000回繰り返すのに要した時間を計ってみた。
| (1)片方向リスト、KPtrPool使用 | (2)KSizPtr、初期サイズ4バイト(デフォルト) | (3)KSizPtr、初期サイズを巨大にしてmemcpyが発生しないようにした | (2) / (1) | データを 4バイトずつ追加 | 3.755秒 | 2.623秒 | 2.320秒 | 0.699倍 | データを 8バイトずつ追加 | 4.392秒 | 3.802秒 | 2.594秒 | 0.866倍 | データを12バイトずつ追加 | 5.227秒 | 6.506秒 | 3.734秒 | 1.245倍 | データを16バイトずつ追加 | 6.161秒 | 7.478秒 | 4.723秒 | 1.214倍 | データを24バイトずつ追加 | 8.046秒 | 12.653秒 | 6.637秒 | 1.573倍 |
- こうしてみると、境目は8バイトと12バイトの間にありそうなことが分かる。
- また(これは当然ではあるが)memcpyが発生しないように初期サイズを大きく取ってやると、可変長配列は事実上の固定長配列になって最速になっている。
- これを踏まえると、結局どうすることがベストなのであろうか。・・・私としては少なくとも16バイトくらいまでは、可変長配列(KSizPtr)がいいのではないかと思う。なぜなら16バイトくらいまでは片方向リストに比べてそんなに遅いわけではないし、もし事前にサイズが大きくなることが予期できるのであればreserveによってあらかじめ大きくしてやってmemcpyの発生を回避して高速化する余地も残っているからである。片方向リストで実装してしまうと、あらかじめ予期できてもそれを高速化につなげる方法はない。
- 一方で16バイトを超えるようだと、サイズが大きくなることを予期できない場合のコスト増がかなりのものになってくるので、これは素直に片方向リストにしたほうがいいのかなと感じている。
- こうしてみると、KIndexHCでは、ハッシュの衝突が起きた時にチェインでつないでいくのではなく、可変長配列に登録していく方がよさそうである。
- 普通のプログラマはmallocよりも18倍くらい高速なKPtrPoolを持っていないので、分岐点は16バイトくらいになっているのかもしれない。
- [2] セキュリティ上のうれしいことで[1]以外のことは、基本的に「プログラムを短く書けるようになった」ということに要約できそうです。プログラムを短く書けるようになったことで見通しが良くなって、バグの発生確率を下げることができるのです。
- 2019.05.22(水)
- [1] 書きたいコードが決まっているのに、時間がなくて書けない・・・。
こめんと欄
|