光学薄膜設計ソフトの設計 その22 - 同じ結果を返すオブジェクト [考え中 - 光学薄膜設計]
光学薄膜計算エンジンの実装の続き。前回まで、計算結果をキャシュするためのメカニズムを考えてきた。そもそもなんでこんな面倒なことをするかと言うと、薄膜の光学特性の計算は同じことを繰り返すことが多いから。
例えば、ある波長帯域で反射防止膜を設計するとする。垂直入射でλn(n = 1..N)で反射率Rがなるべく小さくなるようにする。λnごとに反射率を計算するオブジェクトをN個作っておけば最適化によって変化するパラメータを持っていない層のマトリクスは再計算する必要がない。これを「これは膜厚が変化してるので計算やり直し、こっちは前回と変わってないので結果使い回し・・・」というふうにいちいち確認しながら計算を進めるように書くのは大変。ミニマルな実装で再計算を防止できるメカニズムとして考えている。
ということで、今回はさらに再計算を減らすためのメカニズムの話。
同じ結果を返すオブジェクト
同じ材料の、同一波長での屈折率は当然同じになる。これを別のオブジェクトにすると同じ結果を得るためにオブジェクトごとに内部で屈折率の計算を繰り返すことになる。これは無駄である。
これを避けるために、同じ結果を返すオブジェクトは完全に同じものにしたい。一般にこれを実現するメカニズムを考えてみる。
オブジェクトの同一性
同じ結果を返すオブジェクトとはどういうものか。例えば屈折率の値は、同じ材料で、かつ同じ波長の場合だけである。同じ波長とはすなわち同じ波長変数オブジェクトを参照している、ということである。
また、ひとつの層OTFLayerの場合は、同じ屈折率、同じ厚さで、しかも波長、入射角、偏光が同じ変数オブジェクトを参照している場合だけである。これは一見ほとんどないように思えるけど、実際の設計では繰り返し多層膜、つまり屈折率の高い材料Hと低い材料Lの組みが繰り返されている薄膜はよく使われ、波長一定で入射角を変えて計算する、等と言う場合はよくある。多くのHとLのそれぞれでその屈折率や特性マトリクスを計算する必要はない。この場合、多くのH、Lは同じオブジェクトを参照していれば特性マトリクスの計算は1回だけで、そのマトリクスの積だけを計算すればよくなる。
膜を組み立てるときに同じ結果を返すオブジェクトは使い回せばいいけど、その管理は非常に面倒なので、自動的にできるようにしたい。そのメカニズムを実現するにはどうすればいいか。
オブジェクトを同一視することが必要となるクラス
今回の計算ですべてのオブジェクトが同一性をいちいちチェックする必要があるかといえばそうではない。前回変数を2種類に分類した。この分類で主変数は別オブジェクトでなければいけないけど、従変数は参照している主変数が同じものであれば同じ結果を返すことになる。ということで同じ変数を参照する従変数オブジェクトはひとつだけになるようにすればいい。前節で従変数の具体的な派生クラスは、OTFDependedVariableを継承した。その間にひとつ抽象クラスを挟むと言うことを考える。その抽象クラスをOTFMergableとしよう。これはOTFDependedVariableを継承した
@interface OTFMergable : OTFDependedVariable + (NSMutableSet *)allocatedObjects; - (NSMutableSet *)allocatedObjectsOfSameClass; - (id)merge; - (BOOL)isMergableTo:(id)obj; - (BOOL)isEqual:(id)obj; @endみたいなもの。クラス変数のNSMutableSetに生成されたオブジェクトが登録され、新しいオブジェクトを作るときに、そこに同じと見なせるオブジェクトがあればそっちを返すことにする。この実装は、まず
static NSMutableSet *allocatedObjects; @implementation OTFMergable + (void)initialize { allocatedObjects = [[NSMutableSet alloc] initWithCapacity:0]; } + (NSMutableSet *)allocatedObjects { return allocatedObjects; } - (NSMutableSet *)allocatedObjectsOfSameClass { return [[self class] allocatedObjects]; }としてNSMutableSetのallocatedObjectsをクラス変数として作る。 そして
- (id)init { self = [super init]; // return [self merge]; return self; } - (id)merge { id identity = [self findMergable]; if (identity) { [self release]; return identity; } else { [[self allocatedObjectsOfSameClass] addObject:self]; return self; } } - (void)dealloc { NSMutableSet *allocated = [self allocatedObjectsOfSameClass]; if ([allocated containsObject:self]) [allocated removeObject:self]; [super dealloc]; }とする。このinitメソッドは意味がないが、サブクラスではかならずmergeメソッドを呼ぶことにする。mergeメソッドはfindMergableメソッドで同じと見さされるオブジェクトがあった場合、自分自身を解放してもとあった方を返す。findMergableメソッドがnilを返すと、それは同じものがない、ということなので自分自身をNSMutableSetに登録して自分を返す。
findMergableメソッドは
- (id)findMergable { NSSet *allocated = [self allocatedObjectsOfSameClass]; NSEnumerator *en = [allocated objectEnumerator]; id obj; while (obj = [en nextObject]) if ([self isMergableTo:obj]) return obj; return nil; }みたいなもの。isMergableTo:メソッドで比較してYESを返すもを同じと見なす。
これは抽象クラスなので
- (BOOL)isMergableTo:(id)obj { return NO; }として、同じものはないとしているが、これを継承した具体的なクラスでiMergableTo:を上書きして同じ変数を参照しているオブジェクトはisMergableTo:にYESを返すようにすればいい。
ここでひとつ
- (BOOL)isEqual:(id)obj { return self == obj; // exactly the same object } @endとして、NSMutableSetでの同一性比較は完全に同じアドレスのオブジェクトとしておかないと、deallocメソッドがうまく動かない。この定義はNSObjectにあるので、継承の途中で上書きされていなければいらないけど、念のため。
実装の問題点
上の実装ではmergeメソッドがかっこわるいけど、それ以外の問題点のひとつはあきらかである。それは、いったん完全に動作するオブジェクトを作っておきながら、同一であると見なされた場合、mergeメソッドでそれを破棄することになる。これは大きなオーバーヘッドである。
効率を考慮すれば、alloc - initでは実体を作らず、必要なことがわかった時点で作ればいい。こういった実装はFoundationフレームワークの中のクラスクラスタになっているクラスはほとんどこうなっているらしい。例えばNSStringはallocではNSPlaceholderStringというのが返され、init...ではそれと異なるオブジェクトが返されている(デバッガで見てみればわかる。ちなみにこれがわかるようにallocとinitの行をわけると、デバッガが「入れ子になってないぜ」と警告を出す)。これはallocではクラスクラスタの中のクラスが決まらず、init...ではじめて具体的なクラスが決まっているからである。NSStringのallocで返されるNSPlaceholderStringはシングルトンと呼ばれ、何回allocしても同じ実体が返る。
今回もallocでは何もしないシングルトンを返し、initで実際に作るなりNSMutableSetの中のもを返すなりを決めればいい。
そうすると美しく、効率も高い。が、しかし非常に実装が面倒である。今回はオブジェクトを作ることよりも計算の方が重い。たくさんのオブジェクトを作ることになるが、計算ではそれをあるていど使い回すことになる。このままにしておいて、実行時に効率の問題が出るようなら考え直すことにする。
コメント 0