CFArrayの歴史的起源と実装原理(再現)



Historical Origin



iOS開発では、NSArrayは非常に重要なデータ構造です。特に、TableView、NSArrayでのデータのキャッシュと更新により、データをキャッシュし、表示データの操作を変更します。 Core Foundationでは、CFArrayとNSArrayが相互に対応しているため、作成者はCoreFoundationライブラリとFoundationライブラリでのネイティブデータ構造の実装に興味を持ったので、それを調べてみましょう。

CFArrayの歴史的起源



NSArrayとCFArrayは フリーダイヤルブリッジopensource.apple.com CFArrayではオープンソースです。これは私たちの研究と研究にもっと役立ちます。に がらんの堂 偉大な神が個人的なツールライブラリを作成する前に、彼はかつてCFArrayの歴史的起源と実装方法を研究していました。この記事を読む前に、前任者の優れたブログ投稿を参照することができます。

アレイ この2005年初頭のドキュメントでは、CFArrayが最初に導入され、そのパフォーマンスレベルがテストされました。 CFArrayとSTLのVectorコンテナのパフォーマンスを比較します。後者の実装はCで配列をカプセル化するものとして理解できるため、パフォーマンスグラフのほとんどの操作は線形です。 CFArrayダイアグラムには、さまざまな場所があります。



946857-9a2fad5d237f5f0c.jpg

946857-7f6b628dcac487bd.jpg

上記の分析からわかるように、ヘッド挿入とテール挿入におけるCFArrayの効率はほぼ一定であり、中間要素の動作は、小さなデータの線形効率からしきい値での線形効率に突然変化し、このジャンプが発生します。 Java8のHashMapでデータ構造が変換される方法を考えずにはいられません。



ObjCの初期には、CFArrayが使用されていました。 そして何度も 実装なので、ヘッドとテールの操作の効率が示されますが、中間の操作は線形です。容量が300000を超えると(実際には262140 = 2 ^ 18になるはずです)、時間計算量は急激に変化します。ソースコードでは、しきい値は__CF_MAX_BUCKETS_PER_DEQUEとして定義されており、特定のコードを確認できます。 CF-550-CFArray.c (2011年版):

1

3

4

5

6

7if(__ CF_MAX_BUCKETS_PER_DEQUEfutureCnt){

// CFStorage参照を作成します

CFStorageRefstore

// CFArrayをストレージに変換します

__CFArrayConvertDequeToStore(array)

store =(CFStorageRef)array-> _ store

}

データがしきい値__CF_MAX_BUCKETS_PER_DEQUEを超えると、データ構造がCFArrayからCFStorageに変換されることがわかります。 CFStorageは、バランスの取れたバイナリツリー構造です。配列への順次アクセスを維持するために、ノードの重みは挿入と回転のために添え字が付けられています。特定の実装は、CFStorageInsertValues操作で確認できます。特定のコードを表示できます CF-368.18-CFStorage.c

2011年以降 CF-635.15-CFArray.c このバージョンでは、CFArrayはデータ構造変換の機能をキャンセルしました。おそらくこれは、ビッグデータでのバイナリツリー構築のタイムジッターの問題を排除するためです。データ構造の説明を直接見てください。

1

3

4

5

6

7

8

9

10

11struct__CFArrayDeque {

uintptr_t_leftIdx //左から添え字の位置まで

Uintptr_t_capacity //現在の容量

}

struct__CFArray {

CFRuntimeBase_base

CFIndex_count //要素の数

CFIndex_mutations //要素ジッター

int32_t_mutInProgress

__strongvoid * _store

}

命名から、CFArrayは単一の両端キューによって実装され、いくつかのコンテナ情報が記録されます。

C配列に関するいくつかの問題

C言語の配列は、データの読み取り、書き込み、および格納のための連続したメモリスペースを開きます。さらに、たとえば、 配列とポインタは同じではありません 。多くの教科書で悪用されていることわざがあります。mallocが使用するメモリ空間の一部は配列と同じです。これは間違っています。最も簡単な説明では、ポインターはスペースの開始位置を格納(ポイント)するためにポインター領域を適用する必要があり、配列の(ヘッダー)はスペースの開始位置への直接アクセスです。また、もっと知りたい ポインタと配列はCで同等ですか? このブログ投稿。

Cの配列の最も重大な欠点は、インデックス0に挿入するときに、すべての要素を移動する必要があることです(memmove()関数の原理)。同様に、最初の要素を削除し、最初の要素の前に要素を挿入すると、 O(n)複雑さの操作 。ただし、配列は多くの場合、読み取りおよび書き込みコンテナーであるため、O(n)操作はかなりの時間オーバーヘッドを引き起こす可能性があります。

現在のリリースでのCFArrayの部分的な実装の詳細

CF-855.17 では、CFArrayの現在のバージョンの実装を確認できます。 CFArrayのドキュメントには、次の説明があります。

CFArrayは、ポインターによって順次アクセスできるコンパクトなコンテナーを実装します。その値には、0からN-1の範囲の整数キー(インデックス添え字)を介してアクセスできます。ここで、Nは配列内の値の数です。あれを呼べ コンパクト その理由は、コンテナが値を削除または挿入するときに、メモリスペースにギャップが残っておらず、アクセス順序が元のキー値のサイズに従って配置されているため、有効な取得セットの範囲が常に整数になるためです。範囲[0、N-1の間]。したがって、特定の値の添え字は、他の要素が配列に挿入または削除されると変更される可能性があります。

配列には次の2つのタイプがあります。 不変 型は、配列が作成された後、配列に要素を追加または削除することはできません。 可変 タイプは、要素を追加または削除できます。可変配列内の要素の数は無制限です(または、使用可能なメモリスペースの量など、CFArrayの外部の制約によってのみ制限されます)。すべてのCoreFoundationコレクションタイプと同様に、配列は要素オブジェクトとの強力な参照関係を維持します。

CFArrayの詳細をさらに明確にするために、CFArrayのいくつかの操作を分析してみましょう。

1

3

4

5

6

7

8

9

10

十一

12

13

14

15

16

17

18

19

20

21

22

2. 3

24

25 //添え字で要素値を照会

constvoid * CFArrayGetValueAtIndex(CFArrayRefarray、CFIndexidx){

//この関数はまだオープンソースではありません

//指定された要素が指定されたCFTypeIDを持つCoreFoundationブリッジクラスと一致することを確認します

CF_OBJC_FUNCDISPATCHV(__ kCFArrayTypeID、constvoid *、(NSArray *)array、objectAtIndex:idx)

//まだオープンソースではありません

//指定されたCFTypeIDを使用してCoreFoundationタイプの正当性を確認します

__CFGenericValidateType(array、__ kCFArrayTypeID)

CFAssert2(0idx && idx__CFArrayGetCount(array)、__ kCFLogAssertion、 '%s():インデックス(%d)が範囲外です'、__ PRETTY_FUNCTION __、idx)

CHECK_FOR_MUTATION(配列)

//メモリ位置から要素を削除します

return__CFArrayGetBucketAtIndex(array、idx)-> _ item

}

//クエリ要素のアドレスを返します

CF_INLINEstruct__CFArrayBucket * __ CFArrayGetBucketAtIndex(CFArrayRefarray、CFIndexidx){

switch(__ CFArrayGetType(array)){

// 2つの配列タイプのみが許可されます

//通常の線形構造に不変、両端キューに対応する変数

case__kCFArrayImmutable:

case__kCFArrayDeque:

//アドレスとインデックスオフセットを取得し、要素アドレスを返します

return__CFArrayGetBucketsPtr(array)+ idx

}

returnNULL

}

CFArrayは、インデックス添え字クエリ操作を通じて、従来の配列の連続するアドレススペースの性質を引き続き継承するため、その時間をO(1)の複雑さに保つことができ、非常に効率的です。

1

3

4

5

6

7

8

9

10

十一

12

13

14

15

16

17

18

19

20

21

22

2. 3

24

25

26

27

28

29

30

31

32

33

3. 4

35

36

37

38

39

40

41

42

43

44

フォーファイブ

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109voidCFArrayInsertValueAtIndex(CFMutableArrayRefarray、CFIndexidx、constvoid * value){

//指定された要素が指定されたCFTypeIDを持つCoreFoundationブリッジと一致することを確認します

CF_OBJC_FUNCDISPATCHV(__ kCFArrayTypeID、void、(NSMutableArray *)array、insertObject:(id)valueatIndex:(NSUInteger)idx)

//指定されたCFTypeIDを使用してCoreFoundationタイプの正当性を確認します

__CFGenericValidateType(array、__ kCFArrayTypeID)

CFAssert1(__ CFArrayGetType(array)!= __ kCFArrayImmutable、__ kCFLogAssertion、 '%s():配列は不変です'、__ PRETTY_FUNCTION__)

CFAssert2(0idx && idx__CFArrayGetCount(array)、__ kCFLogAssertion、 '%s():インデックス(%d)が範囲外です'、__ PRETTY_FUNCTION __、idx)

//タイプチェック

CHECK_FOR_MUTATION(配列)

//この関数を呼び出して、特定の配列変更プロセスを実行します

_CFArrayReplaceValues(array、CFRangeMake(idx、0)、&value、1)

}

//この関数はObjC、つまりCF_OBJC_FUNCDISPATCHVメソッドによってチェックされていません

//したがって、セキュリティ上の理由から、スケジュールチェック用にスケジュールされた関数エントリの後にのみ使用できます。

void_CFArrayReplaceValues(CFMutableArrayRefarray、CFRangerange、constvoid ** newValues、CFIndexnewCount){

//さらに型チェック

CHECK_FOR_MUTATION(配列)

//ロック操作、スピンロックを増やして競合を防ぎます

BEGIN_MUTATION(配列)

//コールバックを宣言します

constCFArrayCallBacks * cb

//オフセット添え字、要素の総数、配列変更後の要素の総数

CFIndexidx、cnt、futureCnt

constvoid ** newv、* buffer [256]

//配列内の要素の数を取得します

cnt = __ CFArrayGetCount(array)

//新しい配列要素の総数=元の配列要素の総数-削除された要素の数+追加された要素の数

futureCnt = cnt-range.length + newCount

CFAssert1(newCountfutureCnt、__ kCFLogAssertion、 '%s():内部エラー1'、__ PRETTY_FUNCTION__)

//配列で定義されたコールバックメソッドを取得します

cb = __ CFArrayGetCallBacks(array)

//割り当ての空きメモリの抽象化を構築します

CFAllocatorRefallocator = __ CFGetAllocator(array)

//必要に応じて新しい要素を保持し、一時バッファを割り当てます

//標準は、新しい要素の数が256を超えるかどうかです

if(NULL!= cb-> retain &&!hasBeenFinalized(array)){

newv =(newCount256)?(constvoid **)buffer:(constvoid **)CFAllocatorAllocate(kCFAllocatorSystemDefault、newCount * sizeof(void *)、0)

if(newv!= buffer && __ CFOASafe)__ CFSetLastAllocationEventName(newv、 'CFArray(temp)')

//新しい要素のデータバッファを追加します

for(idx = 0idxnewCountidx ++){

newv [idx] =(void *)INVOKE_CALLBACK2(cb-> retain、allocator、(void *)newValues [idx])

}

}そうしないと{

newv = newValues

}

//データジッタは自己加算されます

配列-> _ mutations ++

//配列のストレージ領域を3つの部分に分割し、それぞれが空の場合があります

// A:インデックス添え字ゼロの位置からrange.location未満の領域まで

// B:着信range.locationエリア

// C:range.location + range.lengthから配列の終わりまで

//インデックス0の位置は、必ずしも使用可能なストレージの最下位ビットにあるとは限らないことに注意してください。変更位置の新しい値の数が古い値の数と異なる場合は、B領域を解放してから置き換える必要があり、AとCの値は状況に応じて異なります。変位する

if(0range.length){

//通常のリリース変更エリア操作

__CFArrayReleaseValues(array、range、false)

}

//エリアBは空になり、再入力する必要があります

if(0){

//判定条件とコードはここに隠されています。

//おそらく、Bエリアのデータが完全に解放されていないなど、他の干渉項目を除外する操作です。

} elseif(NULL == array-> _ store){

//データの最初のアドレスにあるポインタを参照して、B領域の解放を決定します

if(0){

//判定条件とコードはここに隠されています

// futureCntが合法ではないなどの干渉条件を排除します。

} elseif(0futureCnt){

//両端キューオブジェクトを宣言します

および* struct__CFArrayDeque

//要素の総数に基づいて、リングバッファ領域で伝送できる要素の総数を決定します

CFIndexcapacity = __ CFArrayDequeRoundUpCapacity(futureCnt)

//スペース割り当てのサイズを決定する要素の数に応じて

CFIndexsize = sizeof(struct__CFArrayDeque)+ capacity * sizeof(struct__CFArrayBucket)

//バッファコンストラクタを介してストレージキャッシュを構築します

deque =(struct__CFArrayDeque *)CFAllocatorAllocate((allocator)、size、isStrongMemory(array)?__ kCFAllocatorGCScannedMemory:0)

if(__ CFOASafe)__ CFSetLastAllocationEventName(and、CFArray(store-and) ')

//両端キューの左辺値を決定します

および-> _ leftIdx =(capacity-newcount)/ 2

deque-> _capacity =容量

__CFAssignWithWriteBarrier((void **)&array-> _ store、(void *)deque)

// B領域の構成を完了し、配列を安全に解放します

if(CF_IS_COLLECTABLE_ALLOCATOR(allocator))auto_zone_release(objc_collectableZone()、deque)

}

} Else {//再カウント

// B領域要素の変更に基づいて、A領域とC領域のストレージ状態を再配置します

if(0){

} elseif(range.length!= newCount){

//配列参照に入力し、最後に変更に応じて配列のA、B、Cパーティションルールを更新します

__CFArrayRepositionDequeRegions(array、range、newCount)

}

}

//新しい変更をエリアBからエリアBにコピーします

if(0newCount){

if(0){

} Else {//再カウント

//線形ストレージ領域にアクセスします

struct__CFArrayDeque * deque =(struct__CFArrayDeque *)array-> _ store

//元のベースでキャッシュ領域を追加します

struct__CFArrayBucket * raw_buckets =(struct__CFArrayBucket *)((uint8_t *)deque + sizeof(struct__CFArrayDeque))

// memcpyと同様に、B領域データを変更しますが、書き込みバリアがあり、スレッドセーフです

objc_memmove_collectable(raw_buckets + deque-> _ leftIdx + range.location、newv、newCount * sizeof(struct__CFArrayBucket))

}

}

//属性の新しい数を設定します

__CFArraySetCount(array、futureCnt)

//キャッシュ領域を解放します

if(newv!= buffer && newv!= newValues)CFAllocatorDeallocate(kCFAllocatorSystemDefault、newv)

//スレッドセーフの保護を解除します

END_MUTATION(配列)

}

CFArrayの要素挿入操作では、これが 両端キュー (デキュー)要素の挿入操作であり、C ++ STL標準ライブラリを格納する方法です。 バッファネスティングマップテーブル 静的実装。回路図を使用して、データ構造を説明します。

946857-beea5f00dfb3733f.png

STLでは、dequeはマッピング関係を記録するために使用されるマップです。 Core Foundationでは、CFArrayは、このような2次マッピングを保証するときに、2次ポインター_storeを直接使用します。要素を変更する操作では、CFArrayもわずかに暴力的です。 最初にアレイでラージブロックパーティション操作を実行し、次にデータを順番に入力して、それらを新しい両端キューに結合します。 たとえば、上の図の両端キューで、添え字7の要素の前に値100の要素を追加します。

946857-2719b59eb5614ffa.png

インデックスの添え字に従って、指定された部分のバッファが検出され、取り出されて再構築されます。建設プロセスでは、A、B、Cの3つの領域に分割されます。B領域は変更された部分です。もちろん、それが十分でない場合、システムはバッファ領域自体、つまりCFAllocatorRefによって提供されるメモリ割り当て/解放戦略を拡張します。

CFAllocatorRefは、CoreFoundationでメモリを割り当てて解放するための戦略です。ほとんどの場合、デフォルトのアロケータkCFAllocatorDefaultを使用するだけで済みます。これは、NULLパラメータを渡すことと同等であり、CoreFoundationのいわゆる「通常のメソッド」を使用してメモリを割り当てて解放します。この方法は変更される可能性があり、特別な動作をするべきではありません。特別なディスペンサーの使用はまれであり、公式ドキュメントに記載されている標準のアロケーターとその機能です。

KCFALLOCATORDEFAULTデフォルトのアロケータ。NULLを渡すのと同じです。

kCFAllocatorSystemDefault元のデフォルトのシステムアロケータ。このアロケータは、CFAllocatorSetDefaultを使用してデフォルトのアロケータへの変更に応答するために使用されますが、これはめったに使用されません。

kCFAllocatorMallocは、malloc、realloc、およびfreeを呼び出します。 mallocを使用してメモリを作成する場合、このアロケータはCFDataとCFStringを解放するのに役立ちます。

kCFAllocatorMallocZoneは、デフォルトのmalloc領域にメモリを作成して解放します。このアロケータは、Macでガベージコレクションがオンになっている場合に便利ですが、iOSでは基本的に役に立ちません。

kCFAllocatorNullは何もしません。 kCFAllocatorMallocと同様に、このアロケータは、メモリを解放したくない場合にCFDataとCFStringを解放するのに役立ちます。

KCFAllocatorUseContextは、CFAllocatorCreate関数によってのみ使用されます。 CFAllocatorを作成するとき、システムはメモリを割り当てる必要があります。他のすべてのCreateメソッドと同様に、アロケーターも必要です。この特別なアロケーターは、渡された関数を使用してCFAllocatorを割り当てるようにCFAllocatorCreateに指示します。

_CFArrayReplaceValuesメソッドの最後の審判:

1

2if(newv!= buffer && newv!= newValues)

CFAllocatorDeallocate(kCFAllocatorSystemDefault、newv)

バッファの数が多すぎると余分なキャッシュが解放される場合は、バッファの数を確認します。これは、このメソッドが用途が広く、要素操作の挿入だけでなく、(CFArrayAppendValue)の追加、(CFArrayReplaceValues)の置換、および(CFArrayRemoveValueAtIndex)操作の削除にも使用できるためです。データ構造のブロック管理により、時間が分散され、複雑さが大幅に軽減されます。したがって、CFArrayの時間計算量は、クエリおよび要素の追加操作でより高いレベルにあることがわかります。

NSMutableArrayの実装では、モバイル端末の小さなメモリ特性を解決するために、CFArrayを使用して両端の拡張可能なバッファ領域を増やすと、多くの無駄が発生します。に NSMutableArrayの原理が明らかに この記事では、NSMutableArrayの実装原理をマイニングするために逆のアイデアが使用されています。 リングバッファ キャッシュ部分の圧縮を最大化することは、モバイルデバイスの制限に対するAppleのソリューションです。

参考資料:

NSMutableArrayを構築しましょう

GNUStep・NSArray

NSMutableArrayの背後にあるデータ構造は何ですか?

Appleソースコード– CF-855.17

1いいね コレクション コメント

946857-7fc550b0893b13c7.jpg

関連記事

NSArrayからクラスターを見る

興味があるかもしれないトピック

Bole Onlineには、良いと思う記事がたくさんあります。 1

インターンシップと1年以上の仕事、少し厄介な、コーミング 1

2年生の妹が助けを求めてきました。将来どうしたらいいのかわかりません。今はフロントエンドに行きたいです。 6

米国のグループのコメント2017年秋の脳卒中テストの質問:スタックの内外 1

米国のグループのコメント2017年秋の脳卒中テストの質問:ハヴェルマンコード 1

US Mission Review2017秋のストロークZhentiZhenti:Binary Tree Node