LLVMのドキュメントを見ると、彼らは 「RTTIのカスタム形式」を使用している であると述べており、これがisa<>
、cast<>
およびdyn_cast<>
テンプレート関数。
通常、ライブラリが言語のいくつかの基本的な機能を再実装していることを読むのは、ひどいコードのにおいであり、ただ実行するように誘います。しかし、これは私たちが話しているLLVMです:みんなC++コンパイラとC++ランタイムで作業しています。 Mac OSに同梱されているclang
バージョンよりgcc
のほうが好きなので、彼らが何をしているかわからない場合は、かなり困惑しています。
それでも、経験が少ないので、通常のRTTIの落とし穴は何なのか疑問に思います。私はそれがvテーブルを持つ型に対してのみ機能することを知っていますが、それは2つの質問を提起するだけです:
virtual
とマークしないでください。仮想デストラクタはこれが得意なようです。LLVMが独自のRTTIシステムを導入する理由はいくつかあります。このシステムはシンプルで強力であり、 LLVMプログラマーズマニュアル のセクションで説明されています。別の投稿者が指摘しているように、 コーディング基準 はC++ RTTIで2つの主要な問題を引き起こします:1)スペースコストと2)それを使用するパフォーマンスの低下。
RTTIのスペースコストは非常に高くなります。vtable(少なくとも1つの仮想メソッド)を持つすべてのクラスは、クラスの名前とその基本クラスに関する情報を含むRTTI情報を取得します。この情報は typeid 演算子と dynamic_cast の実装に使用されます。このコストは、vtableを使用するすべてのクラスに対して支払われるため(そしてvtableがRTTI情報を指すため、PGOおよびリンク時の最適化は役に立ちません)、LLVMは-fno-rttiを使用して構築されます。経験的に、これにより実行可能ファイルのサイズの約5〜10%を節約できます。 LLVMはtypeidに相当するものを必要としないため、各クラスの名前を(type_infoの中で特に)保持することは、スペースの無駄遣いです。
パフォーマンスの低下は、ベンチマークを行ったり、単純な操作で生成されたコードを調べたりすると、非常に簡単にわかります。 LLVM isa <>演算子は、通常、単一のロードと定数との比較にコンパイルされます(クラスは、classofメソッドの実装方法に基づいてこれを制御します)。これは簡単な例です:
#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return isa<ConstantInt>(V); }
これは次のようにコンパイルされます。
$ clang t.cc -S -o--O3 -I $ HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer ... __Z13isConstantIntPN4llvm5ValueE: cmpb $ 9、8(%rdi) sete%al movzbl%al、%eax ret
これは(アセンブリを読み取らない場合)負荷であり、定数と比較します。対照的に、dynamic_castと同等のものは次のとおりです。
#include "llvm/Constants.h"
using namespace llvm;
bool isConstantInt(Value *V) { return dynamic_cast<ConstantInt*>(V) != 0; }
これは次のようにコンパイルされます。
clang t.cc -S -o--O3 -I $ HOME/llvm/include -D__STDC_LIMIT_MACROS -D__STDC_CONSTANT_MACROS -mkernel -fomit-frame-pointer ... __ Z13isConstantIntPN4llvm5ValueE : pushq%rax xorb%al、%al testq%rdi、%rdi je LBB0_2 xorl%esi、%esi movq $ -1、%rcx xorl%edx、%edx callq ___ dynamic_cast testq%rax、%rax setne%al LBB0_2: movzbl%al、%eax popq%rdx ret
これははるかに多くのコードですが、キラーは__dynamic_castへの呼び出しであり、RTTIデータ構造を調べて、非常に一般的な動的に計算されたウォークスルーを実行する必要があります。これは、ロードおよび比較よりも数桁遅いです。
わかりました、それで遅いので、なぜこれが問題なのですか? LLVMがタイプチェックのLOTを行うため、これは重要です。オプティマイザの多くの部分は、コード内の特定の構造にパターンマッチングし、それらの置換を実行することを中心に構築されています。たとえば、次のコードは、単純なパターンと照合するためのコードです(Op0/Op1が整数の減算演算の左側と右側であることはすでにわかっています)。
// (X*2) - X -> X
if (match(Op0, m_Mul(m_Specific(Op1), m_ConstantInt<2>())))
return Op1;
一致演算子とm_ *は、一連のisa/dyn_cast呼び出しに要約されるテンプレートメタプログラムであり、それぞれが型チェックを行う必要があります。この種のきめの細かいパターンマッチングにdynamic_castを使用すると、残酷で見栄えが遅くなります。
最後に、もう1つ、表現力のポイントがあります。 LLVMが使用する 異なる 'rtti'演算子 は、タイプチェック、動的キャスト、強制(アサート)キャスト、null処理などのさまざまな表現に使用されます。C++の動的キャストはこれを(ネイティブに)提供しません機能性。
結局、この状況を見るには2つの方法があります。マイナス面としては、C++ RTTIは、多くの人が望むもの(完全な反映)に対して非常に狭く定義されており、LLVMのような単純なものでさえも遅すぎるためです。肯定的な面では、C++言語は強力であり、このような抽象化をライブラリコードとして定義し、言語機能の使用をオプトアウトできます。 C++について私の好きな点の1つは、ライブラリがいかに強力で洗練されているかです。 RTTIは、C++の私のお気に入りの機能のうち、それほど高くはありません:)!
-クリス
LLVMコーディング標準 はこの質問にかなりよく答えているようです:
コードと実行可能ファイルのサイズを削減するために、LLVMはRTTI(例:dynamic_cast <>)または例外を使用しません。これら2つの言語機能は、「使用した分だけ支払う」というC++の一般原則に違反しているため、コードベースで例外が使用されなかったり、クラスでRTTIが使用されなかったりしても、実行可能肥大化を引き起こします。このため、コード内でグローバルにオフにします。
そうは言っても、LLVMはisa <>、cast <>、dyn_cast <>などのテンプレートを使用するRTTIの手巻き形式を幅広く利用しています。この形式のRTTIはオプトインであり、任意のクラスに追加できます。また、dynamic_cast <>よりも大幅に効率的です。
ここ はRTTIに関する素晴らしい記事であり、独自のバージョンをロールする必要がある理由について説明しています。
私はC++ RTTIの専門家ではありませんが、自分でRTTIを実装する必要があります。まず、C++ RTTIシステムはそれほど機能が豊富ではありません。基本的に、型キャストと基本情報の取得しかできません。実行時に、クラスの名前を含む文字列があり、そのクラスのオブジェクトを構築したい場合は、C++ RTTIでこれを実行してください。また、C++ RTTIは実際には(または簡単に)モジュール間で移植可能ではありません(別のモジュール(dll/soまたはexe)から作成されたオブジェクトのクラスを識別できません)。同様に、C++ RTTIの実装はコンパイラに固有であり、通常、これをすべてのタイプに実装するための追加のオーバーヘッドの点でオンにするのはコストがかかります。最後に、永続的ではないため、たとえばファイルの保存/ロードに実際に使用することはできません(たとえば、オブジェクトのデータをファイルに保存しますが、そのクラスの「typeid」も保存する必要があります。これにより、ロード時に、このデータをロードするために作成するオブジェクトがわかり、C++では確実に実行できません。 RTTI)これらの理由のすべてまたはいくつかのために、多くのフレームワークには独自のRTTIがあります(非常に単純なものから非常に機能が豊富なものまで)。例としては、wxWidget、LLVM、Boost.Serializationなどがあります。これは、それほど珍しいことではありません。
Vtableを作成するには仮想メソッドが必要なだけなので、メソッドを仮想としてマークしないでください。仮想デストラクタはこれが得意なようです。
それはおそらく彼らのRTTIシステムも使用しているものです。仮想関数は動的バインディング(ランタイムバインディング)の基礎となるため、基本的には、あらゆる種類のランタイムタイプの識別/情報を実行するために必要です(C++ RTTIで必要なだけでなく、RTTIの実装では何らかの方法で仮想呼び出しに依存します)。
彼らのソリューションが通常のRTTIを使用していない場合、それがどのように実装されているかについての考えはありますか?
もちろん、C++でRTTI実装を検索できます。私は自分で作成しましたが、独自のRTTIを持つライブラリもたくさんあります。本当に書くのはとても簡単です。基本的に、必要なのは、タイプ(つまり、クラスの名前、またはそのマングルバージョン、または各クラスの一意のIDなど)を一意に表す手段であり、type_info
に類似したある種の構造です。必要なタイプに関するすべての情報が含まれている場合、要求時にこのタイプ情報を返す各クラスに「非表示」仮想関数が必要です(この関数が各派生クラスでオーバーライドされている場合、機能します)。もちろん、すべてのタイプのシングルトンリポジトリのように、実行可能ないくつかの追加の処理があります(関連付けられたファクトリ関数を使用する場合があります(これは、実行時に既知のすべてが名前である場合にタイプのオブジェクトを作成するのに役立ちます)文字列またはタイプIDとしてのタイプの)。また、動的な型キャストを可能にする仮想関数を追加することもできます(通常、これは、最も派生したクラスのキャスト関数を呼び出し、キャストしたい型までstatic_cast
を実行することによって行われます)。
主な理由は、メモリ使用量を可能な限り低く保つのに苦労していることです。
RTTIは、少なくとも1つの仮想メソッドを備えたクラスでのみ使用できます。つまり、クラスのインスタンスには仮想テーブルへのポインターが含まれます。
64ビットアーキテクチャ(今日では一般的)では、1つのポインタは8バイトです。コンパイラーはたくさんの小さなオブジェクトをインスタンス化するので、これはすぐに追加されます。
したがって、仮想関数を可能な限り(そして実用的に)削除し、仮想関数であるものをswitch
命令で実装するための継続的な取り組みがあります。これは、実行速度は同じですが、メモリへの影響は大幅に低くなります。
たとえば、Clangが消費するメモリはgccよりも大幅に少ないため、クライアントにライブラリを提供する場合に重要です。
一方、新しい種類のノードを追加すると、各スイッチを調整する必要があるため、通常は適切な数のファイルでコードが編集されることになります(スイッチの列挙型メンバーを逃した場合、コンパイラは警告を発行します)。そのため、メモリ効率の点でメンテナンスを少し難しくすることを受け入れました。