1. サイトトップ
  2. ブログ
  3. C++
  4. 【C++】C++で静的ポリモーフィズムを確認してみる

【C++】C++で静的ポリモーフィズムを確認してみる

こんにちは、情熱開発部・プログラム課の青柳です。
異世界おじさんアニメをたのしく見ています。
おじさんいいですね、魔法が使えて強くて粘り強いが残念で。

現実の私もおじさんですが魔法は使えません。
そんな私もかつては魔法使いになりたいと思っていたことがありました。
結局、魔法使いにはなれなかったのですがさておき。
プログラマでC++で魔法といえば、そうテンプレートメタプログラミングですね。
今回はテンプレートを使って簡単な静的ポリモーフィズムを見ていきましょう。Visual Studio2022 バージョン17.1.5C++言語標準は ISO C++20(/std:C++20)にて確認します。

動的ポリモーフィズム、静的ポリモーフィズムとは

継承を使ったポリモーフィズム

C++の継承を勉強した際にポリモーフィズムという単語を目にされたと思います。
ポリモーフィズムとは日本語では多態性などと訳され、共通の性質を提供しそれぞれ異なる型でも統一的に扱える仕組みになります。
ここでは特に仮想関数で実現されたメソッドを継承した際の動きに注目します。
継承を使ったポリモーフィズムを動的ポリモーフィズムとします。

テンプレートを使ったポリモーフィズム

テンプレートを使うとある場合にクラスが満たすべき性質をコンパイル時にチェック出来ます。
コンパイル時にチェック出来るために静的、という事で静的ポリモーフィズムと呼ばれています。

静的ポリモーフィズムの利点

一番で明確な利点は動作が軽量になる事です。
動的ポリモーフィズムの仮想関数呼び出しと比べて動作が軽量になります。

仮想関数は一般的に仮想関数テーブルという形で継承を行った型のメソッドがどこにあるかを保存しどのメソッドを呼ぶかを決めています。
この形ですとメモリの局所性が悪くなってしまいます。

静的ポリモーフィズムならば普通の関数呼び出しですので仮想関数のパフォーマンス上のデメリットは受けません。

では実際に簡単な例を見てみましょう。

静的ポリモーフィズムの簡単な例

template<typename T>
void executeUpdate(T& value, float elapsedTime)
{
    // floatを引数に持つupdateメソッドを要求している
	value.update(elapsedTime);
}

struct UpdatableA
{
	void update(float elapsedTime)
	{
		std::cout << "updateA" << std::endl;
	}
};

struct UpdatableB
{
	void update(float elapsedTime)
	{
		std::cout << "updateB" << std::endl;
	}
};

struct CanNotUpdate
{
};

int main()
{
	UpdatableA updatableA;
	UpdatableB updatableB;
	CanNotUpdate canNotUpdate;


	executeUpdate(updatableA, 0.0f);
	executeUpdate(updatableB, 0.0f);
	//executeUpdate(canNotUpdate, 0.0f); //コンパイルエラー!

	return 0;
}

実行結果

updateA
updateB

こちらのようにexecuteUpdateはテンプレートでなんでも入れられる、ように見えて引数になれる型はfloatを引数に持つupdateメソッドがある事を要求しています。
ない場合、コンパイラがupdateメソッドがありませんとコンパイルエラーを返してきます。
executeUpdateの実体を型ごとにコンパイラが作る際に、作れなかったという事ですね。

conceptsを使ってちょっと書きなおす

C++20でconceptsというキーワードが追加されました。conceptsを使って上のプログラムをすこし書き換えてみます。

#include <concepts>

template<typename T>
concept NeedUpdate = requires(T& value, float elapsedTime)
{
	{value.update(elapsedTime)} -> std::convertible_to<void>;
};

template<NeedUpdate T>
void executeUpdate(T& value, float elapsedTime)
{
	value.update(elapsedTime);
}

C ++20コンセプト入門以前にて

また cpprefjp – C++日本語リファレンス では次のように要約しています。これなら分かりやすいでしょうか?

>> C++20から導入される「コンセプト (concepts)」は、テンプレートパラメータを制約する機能である。

C ++20コンセプト入門以前

とあります。
conceptsを使う事でexecuteUpdateが満たすべき制約を明確に指定できます。
今まで黒魔術としか言いようがない記述が多少分かりやすくなったはずです。
これが今回改めて静的ポリモーフィズムを調べて見ようと思ったモチベーションです。

静的ポリモーフィズムでどれくらい早くなるか確認する

動的ポリモーフィズムと比べてどれくらい静的ポリモーフィズムで早くなるのか確認したいと思います。
ここで一点問題があるのですが静的ポリモーフィズムで実現した多態なクラス達を一つのstd::vectorなどに入れる事は出来ません。
速くなる予測はありますがstd::vectorで統一的に扱えないのではすこし片手落ちな面もあります。
そこで今回は静的ポリモーフィズムしつつstd::vectorにまとめられるライブラリを使って処理を計測します

proxyの紹介

使用するライブラリのproxyを紹介します。Microsoft様がGitHubで公開しているライブラリになります。
proxy: Runtime Polymorphism Made Easier Than Ever
pro::dispatchでメソッドなどの制約を定義して。
pro::facadeでコピー可能性、などを制約し。
pro::proxyでそれらの制約が同じなら同じ型として扱える様になります。
なりますと書きましたが私も頑張って読んでいる最中です、やはり黒魔術。

処理を計測してみる

以下のプログラムで処理時間を計測してみます。
Visual Studioの設定はReleaseビルドのデフォルト/O2で行います。
違いが分かりやすいように百万個のインスタンスを作ってみます。

#include <concepts>
#include <iostream>
#include <vector>
#include <chrono>
#include "proxy.h"

// 規約1
struct Draw : pro::dispatch<void()> {
	template <class T>
	void operator()(T& self) { self.Draw(); }
};
// 規約2
struct Area : pro::dispatch<double()> {
	template <class T>
	double operator()(const T& self) { return self.Area(); }
};

// クラスタイプ
struct DrawableFacade : pro::facade<Draw, Area> {
	static constexpr std::size_t maximum_size = sizeof(void*);
	static constexpr auto minimum_copyability = pro::constraint_level::nothrow;
};

// 静的ポリモーフィズムクラス
class StaticDrawA
{
public:
	StaticDrawA(int value) 
		: value_(value)
	{
	}

	void Draw()
	{
		++value_;
	}

	double Area() const
	{
		return static_cast<double>(value_);
	}

private:
	int value_;
};

// 静的ポリモーフィズムクラス
class StaticDrawB
{
public:
	StaticDrawB(double value)
		: value_(value)
	{
	}

	void Draw()
	{
		++value_;
	}

	double Area() const
	{
		return value_;
	}

private:
	double value_;
};

// 動的ポリモーフィズム元クラス
class IDynamicDraw
{
public:
	virtual ~IDynamicDraw() {};
	virtual void Draw() = 0;
	virtual double Area() const = 0;
};
// 動的ポリモーフィズム派生クラス
class DynamicDrawA : public IDynamicDraw
{
public:
	DynamicDrawA(int value)
		: value_(value)
	{
	}

	void Draw() override
	{
		++value_;
	}

	double Area() const override
	{
		return static_cast<double>(value_);
	}

private:
	int value_;
};
// 動的ポリモーフィズム派生クラス
class DynamicDrawB : public IDynamicDraw
{
public:
	DynamicDrawB(double value)
		: value_(value)
	{
	}

	void Draw() override
	{
		++value_;
	}

	double Area() const override
	{
		return value_;
	}

private:
	double value_;
};



int main()
{
	
	std::vector< pro::proxy<DrawableFacade>> staticDrawables;
	std::vector< IDynamicDraw* > dynamicDrawables;

	constexpr unsigned InstanceNum = 1000000;
    // 交互にA,Bを生成する
	for (int index = 0; index < InstanceNum; ++index)
	{
		if(index % 2 == 0)
		{
			{
				StaticDrawA* instance = new StaticDrawA(index);
				staticDrawables.push_back(instance);
			}

			{
				DynamicDrawA* instance = new DynamicDrawA(index);
				dynamicDrawables.push_back(instance);
			}
		}
		else
		{
			{
				StaticDrawB* instance = new StaticDrawB(index);
				staticDrawables.push_back(instance);
			}

			{
				DynamicDrawB* instance = new DynamicDrawB(index);
				dynamicDrawables.push_back(instance);
			}
		}
	}

	std::chrono::system_clock::time_point start, end;	// 処理時間計測用変数
	double staticElapsed = 0;
	double dynamicElapsed = 0;
	{
		start = std::chrono::system_clock::now();	// 計測開始

		for (int index = 0; index < staticDrawables.size(); ++index)
		{
			auto& staticDraw = staticDrawables.at(index);
			staticDraw.invoke<Draw>();
			staticDraw.invoke<Area>();
		}

		end = std::chrono::system_clock::now();		// 計測終了
		double elapsed = (double)std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000.0;
		staticElapsed += elapsed;

		std::cout << "static : " << staticElapsed << "[ms]\n";
	}

	{
		start = std::chrono::system_clock::now();	// 計測開始

		for (int index = 0; index < dynamicDrawables.size(); ++index)
		{
			auto* dynamicDraw = dynamicDrawables.at(index);
			dynamicDraw->Draw();
			dynamicDraw->Area();
		}

		end = std::chrono::system_clock::now();		// 計測終了
		double elapsed = (double)std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() / 1000.0;
		dynamicElapsed += elapsed;

		std::cout << "dynamic : " << dynamicElapsed << "[ms]\n";
	}

	
	for (int index = 0; index < staticDrawables.size(); ++index)
	{
		auto& staticDraw = staticDrawables.at(index);
		staticDraw.reset();
	}
	staticDrawables.clear();

	for (int index = 0; index < dynamicDrawables.size(); ++index)
	{
		auto* dynamicDraw = dynamicDrawables.at(index);
		delete dynamicDraw;
	}
	dynamicDrawables.clear();
}

それぞれ10回実行しました。

種類平均値[ms]最小値 [ms] 最大値 [ms]
静的ポリモーフィズム 5.618 3.465 6.423
動的ポリモーフィズム 10.3598 7.59 14.712

やはり値にばらつきはありますが静的ポリモーフィズムの方が早いです。
静的ポリモーフィズムの力は素晴らしいぞ!

メモリの局所性をあげて測定してみる

多量なインスタンスに何かさせたい場合、最適化として確保するメモリの場所をまとめてキャッシュヒットを上げる方法があります。こちらの場合でも測定してみます。
こちらもそれぞれ10回実行しました。

種類平均値[ms]最小値 [ms] 最大値 [ms]
静的ポリモーフィズム 4.0283 2.838 7.352
動的ポリモーフィズム 4.4084 3.297 7.343

だいぶ様子が違ってきます。
場合によっては静的ポリモーフィズムの方が遅い結果もありました。
静的ポリモーフィズム の力と同等だと!?

結果を受けて

concepts導入を機会に改めて静的ポリモーフィズムを試してみました。
キャッシュヒットを上げる方法での測定結果はかなり意外でした。
これを見ると静的ポリモーフィズムを採用さえすればパフォーマンス向上!といううまい話ではない事が分かります。

またこういう簡単なプログラムでもproxyライブラリの扱いにはすこし迷うところがあります。
テンプレートメタプログラミングはパラダイムシフトな面は確かにあり、そこが魅力でもあります。
ただ導入して本当に問題が解決するか、もっと別の方法が無いかは考えた方がよさそうです。

しかし、使わなければ使えるようにならないの精神が自分にはあるのと、何より魔法使いになりたい熱は再燃してきました。
出会った場合に困らないような準備をしつつ、プログラムを楽しみたいと思います。

参考資料

proxy: Runtime Polymorphism Made Easier Than Ever
C++20コンセプト入門以前
cpprefjp コンセプト
静的ポリモーフィズムの安全で簡単な実装 -動的から静的にしてパフォーマンス向上-


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。