私の質問は、基本的にQVector
を選択するタイミングと、QtコンテナーとしてQList
を選択するタイミングです。私がすでに知っていること:
ほとんどの場合、QListは適切なクラスです。そのインデックスベースのAPIは、QLinkedListのイテレータベースのAPIよりも便利です。また、アイテムをメモリに格納する方法のため、通常はQVectorよりも高速です。また、実行可能ファイルのコードが少なくなります。
同じことが、この非常に人気のあるQ&Aである QVector vs QList と書かれています。また、QListを優先します。
しかし:最近のQt World Summit 2015でKDABは「QListが有害である理由」を発表しましたが、これは基本的にここにあります:
QListを使用せず、Q_DECLARE_TYPEINFOを使用
私が理解している限り、新しい要素をヒープに割り当てる場合、ほとんどすべての型のQList
は非効率的です。新しい要素を追加するたびに、new
が呼び出され(要素ごとに1回)、これはQVector
に比べて非効率的です。
これが、今私が理解しようとしている理由です。デフォルトのコンテナとして選択する必要があるのはQVector
ですか?
QtはQList
を「すべての取引のジャック」として宣伝しますが、そのことわざの残りの半分は「マスターオブナイン」です。 QList
は前後のスペースを予約するため、リストの両端に追加することを計画している場合、QList
は適切な候補と言えます。それだけです、QList
を使用する正当な理由に関する限りです。
QList
は、ポインタとして「大きな」オブジェクトを自動的に格納し、オブジェクトをヒープに割り当てます。これは、_QVector<T*>
_を宣言して動的割り当てを使用する方法を知らない赤ん坊の場合は、良いことと考えられます。これは必ずしも良いことではありません。場合によっては、メモリ使用量が膨張し、間接参照が追加されるだけです。 IMOそれがポインタであろうとインスタンスであろうと、あなたが望むものについて明示することは常に良い考えです。ヒープの割り当てが必要な場合でも、オブジェクトを一度作成してからヒープにコピーを作成するよりも、自分で割り当てて、リストにポインタを追加するだけの方が常に優れています。
Qtは、たとえばQList
の子を取得するときや、子を検索するときなど、オーバーヘッドが伴う多くの場所でQObject
を返します。この場合、最初の要素の前にスペースを割り当てるコンテナーを使用しても意味がありません。これは、既に存在するオブジェクトのリストであり、先頭に追加する可能性のあるものではないためです。また、resize()
メソッドがないことはあまり好きではありません。
64ビットシステムでサイズが9バイトのオブジェクトとバイトアラインメントがある状況を想像してみてください。 QList
の場合は「多すぎる」ので、代わりに、8バイトのポインタ+遅いヒープ割り当て用のCPUオーバーヘッド+ヒープ割り当て用のメモリオーバーヘッドを使用します。 2倍のメモリを使用し、アクセスのための追加の間接指定があるため、宣伝どおりのパフォーマンス上の利点はほとんどありません。
なぜQVector
が突然「デフォルト」のコンテナになれない-レース中に馬を変更しない-それはレガシーなものであり、Qtはそのような古いフレームワークであり、多くのものは廃止されているにもかかわらず、広く使用されているデフォルトが常に可能であるとは限らず、多くのコードを壊したり、望ましくない動作を引き起こしたりすることもありません。良いか悪いか、QList
は、Qt 5全体を通じてデフォルトのままである可能性が高く、次のメジャーリリースでも同様です。スマートポインターが必須になり、誰もがプレーンポインターがどれほどひどく、どのように使用してはならないかについて誰もが泣いている後、Qtが「ダム」ポインターを引き続き使用する同じ理由。
そうは言っても、設計でQList
を使用するようにyoを強制する人はいません。 QVector
をyourのデフォルトコンテナにしない理由はありません。私自身はどこでもQList
を使用していません。また、QList
を返すQt関数では、一時的なものとして単にQVector
にデータを移動します。
さらに、これは私の個人的な意見にすぎませんが、Qtには、パフォーマンスやメモリの使用効率、使いやすさの点で、必ずしも意味のない多くの設計上の決定があり、フレームワークはたくさんあります。そしてそれはそれを行うための最良の方法ではなく、それを行う彼らの方法だからです。
最後だが大事なことは:
ほとんどの目的で、QListは適切なクラスです。
これは、実際にこれをどのように理解するかにかかっています。この文脈でのIMOは、「右」は「最高」または「最適」を表すのではなく、「十分ではない」のように「十分に良い」を表します。特に、さまざまなコンテナークラスとそれらがどのように機能するかについて何も知らなければ.
ほとんどの目的では、QListが行います。
物事を要約するには:
QList
PRO
QVector
を簡単に使用して、同じで安価な-追加のコピーを作成しないため、サイズを変更するときから)リスト。オブジェクトは移動されず、ポインタのみが移動されますQList
CONs
resize()
メソッドがなく、reserve()
は巧妙なトラップです。インデックスアクセスが機能してもUBカテゴリに分類されても、有効なリストサイズが増加しないため、あなたもそのリストを反復することはできませんCONはPROをわずかに上回ります。つまり、「通常」の使用ではQList
は許容できるかもしれませんが、CPU時間やメモリ使用量が重要な要素である状況では絶対に使用したくないということです。全体として、QList
は、通常は_QVector<T>
_、_QVector<T*>
_であるユースケースに最適なストレージコンテナーを考慮しない場合、遅延および不注意な使用に最適です。またはQLinkedList
(「STL」コンテナは除外します。ここではQtについて説明しているので、Qtコンテナは同じように移植可能であり、場合によってはより高速で、最も簡単で使いやすいですが、std
コンテナは不必要に冗長です)。
QList
はvoid*
の配列です。
通常の操作では、ヒープ上の要素をnew
sし、それらへのポインタをvoid*
配列に格納します。リンクされたリストと同様に、リストに含まれる要素への参照(ただし、リンクされたリストとは異なり、イテレータではありません!)は、要素がコンテナから再び削除されるまで、すべてのコンテナ変更の下で有効なままです。したがって、「リスト」という名前です。このデータ構造は配列リストと呼ばれ、すべてのオブジェクトが参照型(Javaなど)である多くのプログラミング言語で使用されます。これは、すべてのノードベースのコンテナーのように、非常にキャッシュに適さないデータ構造です。
ただし、配列リストのサイズ変更は、タイプに依存しないヘルパークラス(QListData
)に因数分解できます。これにより、実行可能コードのサイズが節約されます。私の実験では、QList
、QVector
、std::vector
のどれが最も実行可能コードが少ないかを予測することはほとんど不可能です。
これは、QString
、QByteArray
など、pimplポインタ以外の何もないQt参照のような多くのタイプに適したデータタイプでした。これらの型の場合、QList
は重要な最適化を得ました。型がポインターより大きくない場合(そしてこの定義は、ヒープ割り当てオブジェクトではなく、プラットフォームのポインターサイズ(32ビットまたは64ビット)に依存することに注意してください)オブジェクトはvoid*
スロットに直接格納されます。
ただし、これが可能なのは、タイプがの場合は、簡単に再配置可能の場合のみです。つまり、memcpy
を使用してメモリに再配置できます。ここでの再配置とは、オブジェクトを別のアドレスにmemcpy
取り、-決定的に-not古いオブジェクトのデストラクタを実行することを意味します。
そして、これは物事がうまくいかなくなったところです。 Javaとは異なり、C++ではオブジェクトへの参照はaddressです。また、元のQList
内では、オブジェクトがコレクションから削除されるまで、参照はvoid*
配列に配置することで安定していたため、このプロパティは保持されません。これは、もはやすべての意図と目的の「リスト」ではありません。
ただし、void*
よりも厳密に小さい型をQList
に配置することも許可されていたため、事態は引き続き悪化しました。ただし、メモリ管理コードはポインタサイズの要素を想定しているため、QList
はpadding(!)を追加します。つまり、64ビットプラットフォームのQList<bool>
は次のようになります。
[ | | | | | | | [ | | | | | | | [ ...
[b| padding [b| padding [b...
QVector
が行うように64のブール値をキャッシュラインにフィッティングする代わりに、QList
は8のみを管理します。
ドキュメントがQList
を適切なデフォルトコンテナと呼ぶようになったとき、問題はあらゆる割合でうまくいきませんでした。そうではありません。 元のSTLの状態 :
Vector
はSTLコンテナークラスの中で最も単純で、多くの場合最も効率的です。
Scott MeyerのEffective STLには、「Prefer std::vector
over ...」で始まる項目がいくつかあります。
Qtを使用しているからといって、C++の一般的な事実が突然間違っているわけではありません。
Qt 6は特定の設計ミスを修正します。それまでは、QVector
またはstd::vector
を使用してください。
QListの要素タイプのサイズがポインターのサイズより大きい場合、QListはオブジェクトを順番に格納するのではなく、ヒープコピーへのポインターを順番に格納するため、QVectorよりもパフォーマンスが高くなります。
私は反対を言う傾向があります。アイテムを通過するとき、それははるかに悪くなるでしょう。それをヒープ上のポインターとして格納する場合、QListはQVectorよりもはるかに悪くなりますか?シーケンシャルストレージ(常にQVector)が非常に優れている理由は、つまり、キャッシュに優しいためです。ポインターを保存すると、データの局所性が失われ、キャッシュミスが発生し始め、パフォーマンスが低下します。
「デフォルト」のコンテナIMHOはQVector(またはstd :: vector)である必要があります。大量の再割り当てが心配な場合は、妥当な量を事前に割り当て、1回限りのコストを支払うと、長期的にはメリットがあります。
パフォーマンスの問題が発生した場合は、デフォルトで* Vectorを使用し、必要に応じてプロファイルおよび変更します。
DataTypeクラスがあるとします。
QVector-以下のようなオブジェクトの配列:
// QVector<DataType> internal structure
DataType* pArray = new DataType[100];
QList-次のようなオブジェクトへのポインターの配列
// QList<DataType> internal structure
DataType** pPointersArray = new DataType*[100];
したがって、QVectorの場合、インデックスによる直接アクセスが高速になります。
{
// ...
cout << pArray[index]; //fast
cout << *pPointersArray[index]; //slow, need additional operation for dereferencing
// ...
}
ただし、sizeof(DataType)> sizeof(DataType *)の場合、QListの方がスワッピングが速くなります。
{
// QVector swaping
DataType copy = pArray[index];
pArray[index] = pArray[index + 1];
pArray[index + 1] = copy; // copy object
// QList swaping
DataType* pCopy = pPointersArray [index];
pPointersArray[index] = pPointersArray [index + 1];
pPointersArray[index + 1] = pCopy; // copy pointer
// ...
}
したがって、要素間の操作(たとえば、並べ替えなど)を交換せずに直接アクセスする必要がある場合、またはsizeof(DataType)<= sizeof(DataType *)の場合は、QVectorを使用するのがより良い方法です。それ以外の場合はQListを使用します。
QListは、ドキュメントの状態として一般的に使用するのに最適なコンテナです。要素のタイプのサイズがポインタのサイズ<=の場合、マシンとOSのビット数= 4または8バイトの場合、オブジェクトはQVectorと同じ方法でメモリに順次格納されます。 QListの要素タイプのサイズがポインターのサイズより大きい場合、QListはオブジェクトを順番に格納するのではなく、ヒープコピーへのポインターを順番に格納するため、QVectorよりもパフォーマンスが高くなります。 32ビットの場合、図は次のとおりです。
sizeof( T ) <= sizeof( void* )
=====
QList< T > = [1][1][1][1][1]
or
[2][2][2][2][2]
or
[3][3][3][3][3]
or
[4][4][4][4][4] = new T[];
sizeof( T ) > sizeof( void* )
=====
QList< T > = [4][4][4][4][4] = new T*[]; // 4 = pointer's size
| | ... |
new T new T new T
通常、OpenGLプログラミングの場合のように、オブジェクトの要素のサイズに関係なく、オブジェクトをメモリ内で順番に配置したい場合は、QVectorを使用する必要があります。
これがQListの内部の 詳細な説明 です。
QListは内部の内容に応じて異なる動作をします( ソースコードstruct MemoryLayout
を参照):
sizeof T == sizeof void*
およびT
がQ_MOVABLE_TYPE
として定義されている場合、QList<T>
はQVector
とまったく同じように動作します。つまり、データは連続してメモリに格納されます。
sizeof T < sizeof void*
およびT
がQ_MOVABLE_TYPE
として定義されている場合、QList<T>
は各エントリをsizeof void*
に埋め込み、QVectorとのレイアウト互換性を失います。
他のすべての場合では、QList<T>
はリンクされたリストであるため、ある程度遅くなります。
この振る舞いがQList<T>
をかなり悪い選択にしている理由です。なぜなら、気の利いた詳細に応じて、QList<T>
は実際にはリストまたはベクトルのいずれかだからです。これは悪いAPI設計であり、エラーが発生しやすくなります。 (たとえば、内部およびそのパブリックインターフェイスでQList<MyType>
を使用するパブリックインターフェイスを持つライブラリがある場合、バグに遭遇します。sizeof MyType is < sizeof void*
。ただし、MyTypeをQ_MOVABLE_TYPE
。その後、Q_MOVABLE_TYPE
を追加する必要があります。これはバイナリ互換ではありません。つまり、パブリックAPIでQList<MyType>
のメモリレイアウトが変更されたため、ライブラリを使用するすべてのコードを再コンパイルする必要があります。注意しないと、これを見逃してバグが発生します。これは、QListがここで不適切な選択である理由をよく表しています。)
とは言っても、QListはまだ完全に悪いわけではありません。おそらく、ほとんどの場合に必要なことを実行しますが、多分それはあなたが期待するものとは異なって舞台裏で仕事をするでしょう。
経験則は:
QListの代わりに、QVector<T>
またはQVector<T*>
を使用します。これは、必要なことを明示的に示しているためです。これをstd::unique_ptr
と組み合わせることができます。
C++ 11以降では、_ 範囲ベースのforループ で正しく動作するため、std::vector
を使用するのが最善と考えられています。 (QVectorとQListは切り離される可能性があるため、ディープコピーを実行します)。
Marc Mutzからのプレゼンテーション および Olivier Goffart によるビデオで、これらすべての詳細などを見つけることができます。