目的
実装方法によってObserverパターンの速度がどれぐらい変わってくるかを知りたい
調査内容
環境
VS 2013 Express
Win32 Releaseビルド
基本形(インターフェース)
Subjectに対してObserverをattachしておけば、
Subjectの値に変化があったときOvserverに対してnotifyが実行される。
変化検知方法も変えたい。
# pragma once
# include <cstdint>
class Observer
{
public:
virtual void notify(const int&, const int&) = 0;
virtual uint64_t count() = 0;
};
class Subject
{
public:
Subject() :cur_(), observer_(){}
void attach(Observer* observer)
{
observer_ = observer;
}
void set(const int& next)
{
if (observer_ != 0){ observer_->notify(cur_, next); }
cur_ = next;
}
private:
int cur_;
Observer* observer_;
};
測定プログラム
上記インターフェースを複数の方法で実現し、下記プログラムで測定した。実現方法は派生形セクションに記載。
全部一度に実行すると実行順などによって遅くなるモノが出てきたので測定対象以外はコメントアウトして1つずつ実行した。
(測定プログラムの書き方が悪いんだろうけど・・・)
結果は「名前:かかった時間, notifyされた回数」という書式にしてある。
# include <iostream>
# include <iomanip>
# include "Timer.hpp"
# include "Observer.hpp"
# include "ObserverBySwitch.hpp"
# include "ObserverInternalStragegy.hpp"
# include "ObserverFP.hpp"
# include "ChangeDetectionObserver.hpp"
# include "TemplatedObserver.hpp"
# include "ObserverT.hpp"
# include "TemplatedSubject.hpp"
# include "Judge.hpp"
template<typename SubType, typename ObType>
class Measure
{
static const uint64_t rep = 100000000;
public:
Measure(){}
double execute(SubType* s)
{
Timer<TimerType::HighAccuracy> timer;
for (uint64_t i = 0; i < rep; ++i)
{
s->set(i);
}
return timer.elapsed();
}
};
# define measure(SubType, ObType) \
{ \
Measure<SubType, ObType> a; \
ObType o; \
SubType s; \
s.attach(&o); \
volatile double r = a.execute(&s); \
auto c = o.count(); \
cout << setiosflags(ios::left) << setw(35) << #ObType":" << fixed << setprecision(6) << r << ", " << c << endl; \
}
int main(int argv, char* argc[])
{
//measure(Subject, ObserverInnerStrategy);
//measure(SubjectFP, ObserverFP);
//measure(Subject, ChangeDetectionObserver);
//measure(Subject, TemplatedObserver<Judge::IsChanged>);
//measure(Subject, TemplatedObserver2<Judge::IsChanged>);
//measure(SubjectT<ObserverT<Judge::IsChanged> >, ObserverT<Judge::IsChanged>);
//measure(SubjectBySwitch, ObserverBySwitch);
/*おまけ。Subjectで扱う型もtemplateにした場合の検証*/
//typedef TemplatedObserverImpl<int, Judge::IsChanged> TemplatedNotifier;
//measure(TemplatedSubject<int>, TemplatedNotifier);
/*判定に対して常にfalseを返す方式と、notify処理自体を空っぽにする方式の比較*/
//typedef NullObserverTImpl<int, Judge::IsChanged> NullNotifier1;
//measure(TemplatedSubject<int>, NullNotifier1);
//typedef TemplatedObserverImpl<int, Judge::Never> NullNotifier2;
//measure(TemplatedSubject<int>, NullNotifier2);
return 0;
}
派生形
ストラテジ
# pragma once
# include <cstdint>
# include "Observer.hpp"
// Observerを継承して内部でストラテジを持っている。
// notifyの継承、judgeの継承で合計2回の仮想関数呼び出しが発生。
// また、ストラテジ部で親クラスのメンバを操作するためにcount_を引き渡すというコストも発生。
class ObserverInnerStrategy: public Observer
{
class NotificationStrategy;
public:
ObserverInnerStrategy() :count_(), notifier_(&isChanged_){}
void setFelld() { notifier_ = &isFell_; }
void setRised() { notifier_ = &isRised_; }
void setChanged(){ notifier_ = &isChanged_; }
void notify(const int& cur, const int& next)override
{
notifier_->notify(cur, next, count_);
}
uint64_t count()override{ return count_; }
private:
uint64_t count_;
class NotificationStrategy
{
public:
void notify(const int& cur, const int& next, uint64_t& c)
{
if (judge(cur, next))
{
++c;
}
}
virtual bool judge(const int& cur, const int& next) = 0;
}* notifier_;
class IsFell : public NotificationStrategy
{
public:
bool judge(const int& cur, const int& next)override
{
return cur && !next;
}
}isFell_;
class IsRised : public NotificationStrategy
{
public:
bool judge(const int& cur, const int& next)override
{
return !cur && next;
}
}isRised_;
class IsChanged : public NotificationStrategy
{
public:
bool judge(const int& cur, const int& next)override
{
return cur != next;
}
}isChanged_;
};
実行結果
ObserverInnerStrategy: 0.347974, 99999999
続行するには何かキーを押してください . . .
関数ポインタで処理をポリモる
# pragma once
# include <cstdint>
//関数ポインタで判定部を実装。
//count_を引き渡さなくてよくなっている。
class ObserverFP
{
class NotificationStrategy;
public:
ObserverFP() :count_(), judge(isChanged){}
void setFelld() { judge = isFell; }
void setRised() { judge = isRised; }
void setChanged(){ judge = isChanged; }
void notify(const int& cur, const int& next)
{
if ((*judge)(cur, next))
{
++count_;
}
}
uint64_t count(){ return count_; }
private:
uint64_t count_;
bool (*judge)(const int& cur, const int& next);
static bool isFell(const int& cur, const int& next)
{
return cur && !next;
}
static bool isRised(const int& cur, const int& next)
{
return !cur && next;
}
static bool isChanged(const int& cur, const int& next)
{
return cur != next;
}
};
class SubjectFP
{
public:
SubjectFP() :cur_(), observer_(){}
void attach(ObserverFP* observer)
{
observer_ = observer;
}
void set(const int& next)
{
if (observer_ != 0){ observer_->notify(cur_, next); }
cur_ = next;
}
private:
int cur_;
ObserverFP* observer_;
};
実行結果
ObserverFP: 0.376251, 99999999
続行するには何かキーを押してください . . .
継承して直接処理を実装しちゃう
# pragma once
# include <cstdint>
# include "Observer.hpp"
// 継承でStrategy部を自分で実装する。
// judgeを自分で持っているのでポインタ経由での関数呼び出しが一回減っている。
class ChangeDetectionObserver : public Observer
{
public:
ChangeDetectionObserver() :count_(){}
void notify(const int& cur, const int& next)override
{
if (judge(cur, next))
{
++count_;
}
}
uint64_t count()override{ return count_; }
private:
uint64_t count_;
bool judge(const int& cur, const int& next)
{
return cur != next;
}
};
実行結果
ChangeDetectionObserver: 0.202233, 99999999
続行するには何かキーを押してください . . .
継承して処理はtemplate引数で変更する
# pragma once
# include <cstdint>
# include "Observer.hpp"
//テンプレートで実装するパターン。
template<typename Judge>
class TemplatedObserver : public Observer
{
public:
TemplatedObserver() :count_(){}
void notify(const int& cur, const int& next)override
{
if (Judge::judge(cur, next))
{
++count_;
}
}
uint64_t count()override{ return count_; }
private:
uint64_t count_;
};
//テンプレートで実装するパターンその2。ファンクタのコンストラクタにコストがかかるのか確認。
template<typename Judge>
class TemplatedObserver2 : public Observer
{
public:
TemplatedObserver2() :count_(){}
void notify(const int& cur, const int& next)override
{
if (Judge()(cur, next))
{
++count_;
}
}
uint64_t count()override{ return count_; }
private:
uint64_t count_;
};
実行結果
TemplatedObserver<Judge::IsChanged>:0.206786, 99999999
続行するには何かキーを押してください . . .
TemplatedObserver2<Judge::IsChanged>:0.204743, 99999999
続行するには何かキーを押してください . . .
Subjectごとtemplate化する
# pragma once
# include <cstdint>
template<typename Judge>
class ObserverT
{
public:
ObserverT() :count_(){}
void notify(const int& cur, const int& next)
{
if (Judge()(cur, next))
{
++count_;
}
}
uint64_t count(){ return count_; }
private:
uint64_t count_;
};
template<typename Observer>
class SubjectT
{
public:
SubjectT() :cur_(), observer_(){}
void attach(Observer* observer)
{
observer_ = observer;
}
void set(const int& next)
{
if (observer_ != 0){ observer_->notify(cur_, next); }
cur_ = next;
}
private:
int cur_;
Observer* observer_;
};
実行結果
ObserverT<Judge::IsChanged>: 0.202937, 99999999
続行するには何かキーを押してください . . .
switch文で実現
# pragma once
# include <cstdint>
//継承なんてしない方式
class ObserverBySwitch
{
public:
enum DetectionType
{
eRised,
eFell,
eChanged,
};
ObserverBySwitch() :count_(), detectionType_(eChanged){}
void setDetectionType(DetectionType detectionType){ detectionType_ = detectionType; }
void notify(const int& cur, const int& next)
{
if (judge(cur, next))
{
++count_;
}
}
uint64_t count(){ return count_; }
private:
bool judge(const int& cur, const int& next)
{
switch (detectionType_)
{
case eRised: return !cur && next;
case eFell: return cur && !next;
case eChanged: return cur != next;
}
return false;
}
uint64_t count_;
DetectionType detectionType_;
};
class SubjectBySwitch
{
public:
SubjectBySwitch() :cur_(), observer_(){}
void attach(ObserverBySwitch* observer)
{
observer_ = observer;
}
void set(const int& next)
{
if (observer_ != 0){ observer_->notify(cur_, next); }
cur_ = next;
}
private:
int cur_;
ObserverBySwitch* observer_;
};
実行結果
ObserverBySwitch: 0.221719, 99999999
続行するには何かキーを押してください . . .
結果まとめ
種類 | 時間[sec] |
---|---|
ChangeDetectionObserver | 0.202233 |
ObserverT<Judge::IsChanged> | 0.202937 |
TemplatedObserver2<Judge::IsChanged> | 0.204743 |
TemplatedObserver<Judge::IsChanged> | 0.206786 |
ObserverBySwitch | 0.221719 |
ObserverInnerStrategy | 0.347974 |
ObserverFP | 0.376251 |
結果からわかること
- 関数ポインタが一番遅い(ObserverFP)
- 仮想関数をなくすのが一番速い。(ObserverT)
- 関数ポインタと継承では、継承の方が速い(ObserverFP > ChangeDetectionObserver)
- メンバ変数や仮想関数を持たないファンクタには生成コストは存在しない(TemplatedObserver ≒ TemplatedObserver2 )
どれが良いのか?
ObserverTは速いがいろんなSubject型ができてしまう。(boost::fusion::vector的なものを使えば同じコンテナに入れて管理できるんだろうけど・・・。)
ChangeDetectionObserverは種類が増えてくると面倒。
TemplatedObserverにしておけば、生成する側が自由に処理を変えられる上に速くて嬉しい。
おまけ
Subjectで扱う型もtemplateにしてみたパターン
# pragma once
# include <cstdint>
# include "Judge.hpp"
template<typename T>
class TemplatedObserverIF
{
public:
virtual void notify(const T& cur, const T& next) = 0;
};
template<typename T, typename Judge>
class TemplatedObserverImpl : public TemplatedObserverIF<T>
{
public:
TemplatedObserverImpl() :count_(){}
void notify(const T& cur, const T& next)
{
if (Judge()(cur, next))
{
++count_;
}
}
uint64_t count(){ return count_; }
private:
uint64_t count_;
};
template<typename T, typename Judge>
class NullObserverTImpl : public TemplatedObserverIF<T>
{
public:
NullObserverTImpl(){}
void notify(const T& , const T& ){}
uint64_t count(){ return 0; }
};
template<typename T>
class TemplatedSubject
{
public:
TemplatedSubject():cur_(), observer_(){}
void attach(TemplatedObserverIF<T>* observer)
{
observer_ = observer;
}
void set(const T& next)
{
if(observer_ != 0)observer_->notify(cur_, next);
cur_ = next;
}
private:
T cur_;
TemplatedObserverIF<T>* observer_;
};
実行結果
TemplatedNotifier: 0.205507, 99999999
続行するには何かキーを押してください . . .
後、なにもしたくないという処理をポリモる場合に、notify実装を空っぽにする方法と、judgeを常にfalseにする方法の比較
実行結果
NullNotifier1: 0.046586, 0
続行するには何かキーを押してください . . .
NullNotifier2: 0.048761, 0
続行するには何かキーを押してください . . .
最適化でif(false)
を消してくれてどっちでも一緒になるっぽい。