上古时代 Objective-C 中哈希表的实现
vbje3051
9年前
<blockquote> <p>因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是 <code>x86_64</code> 架构下运行的,对于在 arm64 中运行的代码会特别说明。</p> </blockquote> <h2>写在前面</h2> <p>文章会介绍上古时代 Objective-C 哈希表,也就是 <code>NXHashTable</code> :</p> <ul> <li><code>NXHashTable</code> 的实现</li> <li><code>NXHashTable</code> 的性能分析</li> <li><code>NXHashTable</code> 的作用</li> </ul> <p><code>NXHashTable</code> 的实现有着将近 30 年的历史,不过仍然作为重要的底层数据结构存储整个应用中的类。</p> <blockquote> <p>文中会涉及一些数据结构方面的简单知识,例如<a href="/misc/goto?guid=4959672782601271013">拉链法</a>。</p> <p>注意:文章中分析的不是 <code>NSHashTable</code> 而是 <code>NXHashTable</code>。</p> </blockquote> <h2>NXHashTable</h2> <p><code>NXHashTable</code> 的实现位于 <code>hashtable2.mm</code> 文件,我们先来看一下 <code>NXHashTable</code> 的结构以及重要的接口:</p> <pre> <code class="language-objectivec">typedef struct { const NXHashTablePrototype *prototype; unsigned count; unsigned nbBuckets; void *buckets; const void *info; } NXHashTable; </code></pre> <p>对于结构体中的 <code>NXHashTablePrototype</code> 属性暂且不说,其中的 <code>buckets</code> 是真正用来存储数据的数组。</p> <pre> <code class="language-objectivec">NXHashTable *NXCreateHashTableFromZone (NXHashTablePrototype prototype, unsigned capacity, const void *info, void *z); unsigned NXCountHashTable (NXHashTable *table); int NXHashMember (NXHashTable *table, const void *data); void *NXHashGet (NXHashTable *table, const void *data); void *NXHashInsert (NXHashTable *table, const void *data); void *NXHashRemove (NXHashTable *table, const void *data); </code></pre> <p>我们会以上面的这些方法作为切入点,分析 <code>NXHashTable</code> 的实现。</p> <h3>NXCreateHashTableFromZone</h3> <p><code>NXHashTable</code> 使用 <code>NXCreateHashTableFromZone</code> 方法初始化:</p> <pre> <code class="language-objectivec">NXHashTable *NXCreateHashTableFromZone (NXHashTablePrototype prototype, unsigned capacity, const void *info, void *z) { NXHashTable *table; NXHashTablePrototype *proto; table = ALLOCTABLE(z); if (! prototypes) bootstrap (); if (! prototype.hash) prototype.hash = NXPtrHash; if (! prototype.isEqual) prototype.isEqual = NXPtrIsEqual; if (! prototype.free) prototype.free = NXNoEffectFree; proto = (NXHashTablePrototype *)NXHashGet (prototypes, &prototype); if (! proto) { proto = (NXHashTablePrototype *) malloc(sizeof (NXHashTablePrototype)); bcopy ((const char*)&prototype, (char*)proto, sizeof (NXHashTablePrototype)); (void) NXHashInsert (prototypes, proto); proto = (NXHashTablePrototype *)NXHashGet (prototypes, &prototype); }; table->prototype = proto; table->count = 0; table->info = info; table->nbBuckets = GOOD_CAPACITY(capacity); table->buckets = ALLOCBUCKETS(z, table->nbBuckets); return table; } </code></pre> <p>在这个方法中,绝大多数代码都是用来初始化 <code>table->prototype</code> 的,我们先把这部分全部忽略,分析一下简略版本的实现。</p> <pre> <code class="language-objectivec">NXHashTable *NXCreateHashTableFromZone (NXHashTablePrototype prototype, unsigned capacity, const void *info, void *z) { NXHashTable *table; NXHashTablePrototype *proto; table = ALLOCTABLE(z); ... table->count = 0; table->info = info; table->nbBuckets = GOOD_CAPACITY(capacity); table->buckets = ALLOCBUCKETS(z, table->nbBuckets); return table; } </code></pre> <p>其中 <code>ALLOCTABLE</code>、<code>GOOD_CAPACITY</code> 以及 <code>ALLOCBUCKETS</code> 都是用来辅助初始化的宏:</p> <pre> <code class="language-objectivec">#define ALLOCTABLE(z) ((NXHashTable *) malloc_zone_malloc ((malloc_zone_t *)z,sizeof (NXHashTable))) #define GOOD_CAPACITY(c) (exp2m1u (log2u (c)+1)) #define ALLOCBUCKETS(z,nb) ((HashBucket *) malloc_zone_calloc ((malloc_zone_t *)z, nb, sizeof (HashBucket))) </code></pre> <p><code>ALLOCTABLE</code> 和 <code>ALLOCBUCKETS</code> 只是调用了 <code>malloc_zone_calloc</code> 来初始化相应的结构体,而 <code>GOOD_CAPACITY</code> 有一些特殊,我们来举个例子说明:</p> <pre> <code class="language-objectivec">c binary result 1 1 1 2 10 3(0b11) 6 110 7(0b111) 100 1100100 127(0b111 1111) </code></pre> <p><code>c</code> 表示传入参数,<code>binary</code> 表示二进制下的参数,而 <code>result</code> 就是 <code>GOOD_CAPACITY</code> 返回的结果。</p> <blockquote> <p>每次返回当前位数下的二进制最大值。</p> </blockquote> <p>获得 <code>table->nbBuckets</code> 之后,再初始化 <code>table->nbBuckets * sizeof (HashBucket)</code>大小的内存空间。</p> <p>NXHashTablePrototype</p> <p>在继续分析其它方法之前,我们需要先知道 <code>NXHashTablePrototype</code> 是什么:</p> <pre> <code class="language-objectivec">typedef struct { uintptr_t (*hash)(const void *info, const void *data); int (*isEqual)(const void *info, const void *data1, const void *data2); void (*free)(const void *info, void *data); int style; /* reserved for future expansion; currently 0 */ } NXHashTablePrototype; </code></pre> <p><code>NXHashTablePrototype</code> 中存储了 <code>hash</code>、<code>isEqual</code> 和 <code>free</code> 的函数指针(用于获取数据的哈希、判断两个数据是否相等以及释放数据)。</p> <p>在 <code>hashtable2.mm</code> 文件中有一个宏 <code>ISEQUAL</code> 就是用了 <code>NXHashTablePrototype</code> 中的 <code>isEqual</code> 来判断两个数据是否相等:</p> <pre> <code class="language-objectivec">#define ISEQUAL(table, data1, data2) ((data1 == data2) || (*table->prototype->isEqual)(table->info, data1, data2)) </code></pre> <p>可以说,<code>NXHashTablePrototype</code> 中存储了一些构建哈希表必要的函数指针。</p> <blockquote> <p>因为 <code>NXHashTable</code> 使用<a href="/misc/goto?guid=4959672782601271013">拉链法</a>来实现哈希表,在存入表前对数据执行 hash,然后找到对应的 buckets,如果与 buckets 中的数据相同(使用 isEqual 判断),就替换原数据,否则将数据添加到链表中。</p> </blockquote> <p>HashBucket</p> <p>在这里另一个需要注意的数据结构就是 <code>HashBucket</code>:</p> <pre> <code class="language-objectivec">typedef struct { unsigned count; oneOrMany elements; } HashBucket; </code></pre> <p><code>oneOrMany</code> 是一个 <code>union</code> 结构体:</p> <pre> <code class="language-objectivec">typedef union { const void *one; const void **many; } oneOrMany; </code></pre> <blockquote> <p>这么设计的主要原因是提升性能。</p> </blockquote> <p>如果 <code>HashBucket</code> 中只有一个元素,那么就直接访问 <code>one</code>,否则访问 <code>many</code>,遍历这个 <code>many</code> 列表。</p> <h3>NXCountHashTable</h3> <p><code>NXCountHashTable</code> 方法应该是我们要介绍的方法中的最简单的一个,它会直接返回 <code>NXHashTable</code> 结构体中的 <code>count</code>。</p> <pre> <code class="language-objectivec">unsigned NXCountHashTable (NXHashTable *table) { return table->count; } </code></pre> <h3>NXHashMember</h3> <p><code>NXHashMember</code> 的函数签名虽然会返回 <code>int</code>,其实它是一个布尔值,会判断当前的 <code>NXHashTable</code> 中是否包含传入的数据:</p> <pre> <code class="language-objectivec">int NXHashMember (NXHashTable *table, const void *data) { HashBucket *bucket = BUCKETOF(table, data); unsigned j = bucket->count; const void **pairs; if (! j) return 0; if (j == 1) { return ISEQUAL(table, data, bucket->elements.one); }; pairs = bucket->elements.many; while (j--) { if (ISEQUAL(table, data, *pairs)) return 1; pairs ++; }; return 0; } </code></pre> <p>使用 <code>BUCKETOF</code> 对 <code>data</code> 进行 hash,将结果与哈希表的 <code>buckets</code> 数取模,返回 <code>buckets</code> 数组中对应的 <code>NXHashBucket</code>。</p> <pre> <code class="language-objectivec">#define BUCKETOF(table, data) (((HashBucket *)table->buckets)+((*table->prototype->hash)(table->info, data) % table->nbBuckets)) </code></pre> <p>在获取了 <code>bucket</code> 之后,根据其中元素个数的不同,选择不同的分支:</p> <pre> <code class="language-objectivec">if (! j) return 0; if (j == 1) { return ISEQUAL(table, data, bucket->elements.one); }; pairs = bucket->elements.many; while (j--) { if (ISEQUAL(table, data, *pairs)) return 1; pairs ++; }; </code></pre> <ul> <li><code>count == 0</code>,直接返回</li> <li><code>count == 1</code>,使用 <code>ISEQUAL</code> 比较查找的数据与 <code>bucket->elements.one</code></li> <li> <p><code>count > 1</code>,依次与 <code>bucket->elements.many</code> 中的值进行比较</p> <blockquote> <p>你可能觉得到这里的时间复杂度比较糟糕,然而这个列表并不会很长,具体会在<a href="/misc/goto?guid=4959672782695249948">NXHashInsert</a> 中解释。</p> </blockquote> </li> </ul> <h3>NXHashGet</h3> <blockquote> <p>其实我一直觉得这个方法可能用处不是很大,尤其是在使用默认的 <code>NXHashTablePrototype</code> 时,因为默认的 <code>NXHashTablePrototype</code> 中的 <code>isEqual</code> 函数指针只是比较两个数据的指针是否相同。</p> <p>其最大作用就是查看当前 <code>data</code> 是不是在表中。</p> <p>如果当前数据在表中,那么这个方法只会返回一个相同的指针,没有太多的意义。</p> </blockquote> <p>它的实现跟上面的 <code>NXHashMember</code> 区别并不大,这里就不过多介绍了:</p> <pre> <code class="language-objectivec">void *NXHashGet (NXHashTable *table, const void *data) { HashBucket *bucket = BUCKETOF(table, data); unsigned j = bucket->count; const void **pairs; if (! j) return NULL; if (j == 1) { return ISEQUAL(table, data, bucket->elements.one) ? (void *) bucket->elements.one : NULL; }; pairs = bucket->elements.many; while (j--) { if (ISEQUAL(table, data, *pairs)) return (void *) *pairs; pairs ++; }; return NULL; } </code></pre> <h3>NXHashInsert</h3> <p><code>NXHashInsert</code> 是 <code>NXHashTable</code> 中比较重要的方法,其作用就是向表中插入数据:</p> <pre> <code class="language-objectivec">void *NXHashInsert (NXHashTable *table, const void *data) { HashBucket *bucket = BUCKETOF(table, data); unsigned j = bucket->count; const void **pairs; const void **newt; if (! j) { bucket->count++; bucket->elements.one = data; table->count++; return NULL; }; if (j == 1) { if (ISEQUAL(table, data, bucket->elements.one)) { const void *old = bucket->elements.one; bucket->elements.one = data; return (void *) old; }; newt = ALLOCPAIRS(z, 2); newt[1] = bucket->elements.one; *newt = data; bucket->count++; bucket->elements.many = newt; table->count++; if (table->count > table->nbBuckets) _NXHashRehash (table); return NULL; }; pairs = bucket->elements.many; while (j--) { if (ISEQUAL(table, data, *pairs)) { const void *old = *pairs; *pairs = data; return (void *) old; }; pairs ++; }; newt = ALLOCPAIRS(z, bucket->count+1); if (bucket->count) bcopy ((const char*)bucket->elements.many, (char*)(newt+1), bucket->count * PTRSIZE); *newt = data; FREEPAIRS (bucket->elements.many); bucket->count++; bucket->elements.many = newt; table->count++; if (table->count > table->nbBuckets) _NXHashRehash (table); return NULL; } </code></pre> <p>虽然这里的实现比上面的两个方法复杂得多,但是脉络仍然很清晰,我们将插入的过程分为三种情况:</p> <ul> <li><code>bucket->count == 0</code></li> <li><code>bucket->count == 1</code></li> <li><code>bucket->count > 1</code></li> </ul> <p>如果对应的 <code>bucket</code> 为空:</p> <pre> <code class="language-objectivec">if (! j) { bucket->count++; bucket->elements.one = data; table->count++; return NULL; }; </code></pre> <p>将数据直接填入 <code>bucket</code>,增加 <code>bucket</code> 中元素的数目,以及 <code>table</code> 中存储的元素的数目:</p> <p><img alt="上古时代 Objective-C 中哈希表的实现" src="https://simg.open-open.com/show/eeef6d32633678045982055ec592ccf0.gif" width="800" height="600"></p> <p>如果原来的 <code>buckets</code> 中有一个元素,它会替换或者使用 <code>many</code> 替换原来的 <code>one</code>:</p> <pre> <code class="language-objectivec">if (j == 1) { if (ISEQUAL(table, data, bucket->elements.one)) { const void *old = bucket->elements.one; bucket->elements.one = data; return (void *) old; }; newt = ALLOCPAIRS(z, 2); newt[1] = bucket->elements.one; *newt = data; bucket->count++; bucket->elements.many = newt; table->count++; ... return NULL; }; </code></pre> <p>当前数据 <code>data</code> 如果与 <code>bucket</code> 中存储的数据相同,就会更新这个数据,否则就会使用 <code>ALLOCPAIRS</code> 初始化一个新的数组,然后将 <code>data</code> 和原来的数据传入。</p> <p><img alt="上古时代 Objective-C 中哈希表的实现" src="https://simg.open-open.com/show/69dacd8a86abedd417c6fd3270c2dd76.gif" width="800" height="600"></p> <p>但是如果原来的 <code>bucket</code> 中存储的元素大于 1,那么会在链表的头部追加一个新的元素:</p> <pre> <code class="language-objectivec">while (j--) { if (ISEQUAL(table, data, *pairs)) { const void *old = *pairs; *pairs = data; return (void *) old; }; pairs ++; }; newt = ALLOCPAIRS(z, bucket->count+1); if (bucket->count) bcopy ((const char*)bucket->elements.many, (char*)(newt+1), bucket->count * PTRSIZE); *newt = data; FREEPAIRS (bucket->elements.many); bucket->count++; bucket->elements.many = newt; table->count++; </code></pre> <p>上面的代码使用 <code>bcopy</code> 将原链表中元素拷贝到新的数组 <code>newt</code> 中。</p> <p><img alt="上古时代 Objective-C 中哈希表的实现" src="https://simg.open-open.com/show/360ab0b40bb9bc0abf26e85f4413d794.gif" width="800" height="600"></p> <p>在每次添加完一个元素之后,都会进行下面的判断:</p> <pre> <code class="language-objectivec">if (table->count > table->nbBuckets) _NXHashRehash (table); </code></pre> <blockquote> <p>上面的这行代码会保证哈希表中的元素数据小于等于表中的 bucket 数量。</p> </blockquote> <p>这就是 <code>buckets</code> 后面的列表非常短的原因,在理想情况下,每一个 <code>buckets</code> 中都只存储一个或零个元素。</p> <p>_NXHashRehash</p> <p>如果哈希表在添加元素后,其中的数据多于 <code>buckets</code> 数量,就会对 <code>NXHashTable</code> 进行 <code>_NXHashRehash</code> 操作。</p> <pre> <code class="language-objectivec">static void _NXHashRehash (NXHashTable *table) { _NXHashRehashToCapacity (table, MORE_CAPACITY(table->nbBuckets)); } </code></pre> <p>它调用 <code>_NXHashRehashToCapacity</code> 方法来扩大 <code>NXHashTable</code> 的容量(<code>HashBucket</code> 的个数)。</p> <pre> <code class="language-objectivec">#define MORE_CAPACITY(b) (b*2+1) </code></pre> <p>而 <code>MORE_CAPACITY</code> 会将当前哈希表的容量翻倍,并将新的容量传入 <code>_NXHashRehashToCapacity</code> 中:</p> <pre> <code class="language-objectivec">void _NXHashRehashToCapacity (NXHashTable *table, unsigned newCapacity) { NXHashTable *old; NXHashState state; void *aux; __unused void *z = ZONE_FROM_PTR(table); old = ALLOCTABLE(z); old->prototype = table->prototype; old->count = table->count; old->nbBuckets = table->nbBuckets; old->buckets = table->buckets; table->nbBuckets = newCapacity; table->count = 0; table->buckets = ALLOCBUCKETS(z, table->nbBuckets); state = NXInitHashState (old); while (NXNextHashState (old, &state, &aux)) (void) NXHashInsert (table, aux); freeBuckets (old, NO); free (old->buckets); free (old); } </code></pre> <ol> <li>创建一个 <code>NXHashTable</code> 的指针指向原哈希表</li> <li>改变哈希表的 <code>nbBuckets</code>,并重新初始化哈希表的 <code>buckets</code> 数组</li> <li>重新将元素插入到哈希表中</li> <li>释放原哈希表 <code>old</code> 以及 <code>buckets</code></li> </ol> <p>NXHashState</p> <p>在将元素重新插入到哈希表中涉及了一个非常奇怪的结构体 <code>NXHashState</code>,这个结构体主要作用是遍历 <code>NXHashTable</code> 中的元素。</p> <pre> <code class="language-objectivec">typedef struct { int i; int j; } NXHashState; </code></pre> <p>我们可以使用如下的代码对哈希表中的元素进行遍历:</p> <pre> <code class="language-objectivec"> unsigned count = 0; MyData *data; NXHashState state = NXInitHashState(table); while (NXNextHashState(table, &state, &data)) { count++; } </code></pre> <p>代码片段中调用了两个方法,分别是 <code>NXInitHashState</code> 以及 <code>NXNextHashState</code>:</p> <pre> <code class="language-objectivec">NXHashState NXInitHashState (NXHashTable *table) { NXHashState state; state.i = table->nbBuckets; state.j = 0; return state; }; </code></pre> <p><code>NXInitHashState</code> 会将 <code>NXHashState</code> 指向哈希表的最末端:</p> <p><img alt="上古时代 Objective-C 中哈希表的实现" src="https://simg.open-open.com/show/f10b5eae5ca2bc6ce2144cff64ab8dc3.png" width="484" height="447"></p> <blockquote> <p>这个位置其实并不属于 <code>NXHashTable</code>,它一定会为空。</p> </blockquote> <p>而每次调用 <code>NXNextHashState</code> 都会向『前』移动一次:</p> <pre> <code class="language-objectivec">int NXNextHashState (NXHashTable *table, NXHashState *state, void **data) { HashBucket *buckets = (HashBucket *) table->buckets; while (state->j == 0) { if (state->i == 0) return NO; state->i--; state->j = buckets[state->i].count; } state->j--; buckets += state->i; *data = (void *) ((buckets->count == 1) ? buckets->elements.one : buckets->elements.many[state->j]); return YES; }; </code></pre> <p>下面的 gif 为我么么展示了每一次调用 <code>NXNextHashState</code> 方法之后当前的 <code>NXHashState</code>:</p> <p><img alt="上古时代 Objective-C 中哈希表的实现" src="https://simg.open-open.com/show/4999d4426d010f4343613b1b94e38ec7.gif" width="800" height="600"></p> <h3>NXHashRemove</h3> <p>这里的 <code>NXHashRemove</code>在某种意义上是 <code>NXHashInsert</code> 的逆操作:</p> <pre> <code class="language-objectivec">void *NXHashRemove (NXHashTable *table, const void *data) { HashBucket *bucket = BUCKETOF(table, data); unsigned j = bucket->count; const void **pairs; const void **newt; __unused void *z = ZONE_FROM_PTR(table); if (! j) return NULL; if (j == 1) { if (! ISEQUAL(table, data, bucket->elements.one)) return NULL; data = bucket->elements.one; table->count--; bucket->count--; bucket->elements.one = NULL; return (void *) data; }; pairs = bucket->elements.many; if (j == 2) { if (ISEQUAL(table, data, pairs[0])) { bucket->elements.one = pairs[1]; data = pairs[0]; } else if (ISEQUAL(table, data, pairs[1])) { bucket->elements.one = pairs[0]; data = pairs[1]; } else return NULL; FREEPAIRS (pairs); table->count--; bucket->count--; return (void *) data; }; while (j--) { if (ISEQUAL(table, data, *pairs)) { data = *pairs; /* we shrink this bucket */ newt = (bucket->count-1) ? ALLOCPAIRS(z, bucket->count-1) : NULL; if (bucket->count-1 != j) bcopy ((const char*)bucket->elements.many, (char*)newt, PTRSIZE*(bucket->count-j-1)); if (j) bcopy ((const char*)(bucket->elements.many + bucket->count-j), (char*)(newt+bucket->count-j-1), PTRSIZE*j); FREEPAIRS (bucket->elements.many); table->count--; bucket->count--; bucket->elements.many = newt; return (void *) data; }; pairs ++; }; return NULL; } </code></pre> <p>它的实现也分为三种情况,不过在这里就不多说了。</p> <h2>NXHashTable 的性能</h2> <p>在已经熟悉了 <code>NXHashTable</code> 的具体实现之后,我们要分析插入不同数据量级的情况下,所需要的时间,这里是主程序的代码,分别测试了在</p> <p><code>100, 1000, 10000, 100000, 1000000, 2000000, 3000000, 5000000, 10000000</code> 数据下 <code>NXHashTable</code> 的性能表现:</p> <p> </p> <pre> <code class="language-objectivec">#import <Foundation/Foundation.h> #import "hashtable2.h" int main(int argc, const char * argv[]) { @autoreleasepool { NSArray<NSNumber *> *capacities = @[ @100, @1000, @10000, @100000, @1000000, @2000000, @3000000, @5000000, @10000000 ]; for (NSNumber *capacity in capacities) { NXHashTable *hashTable = NXCreateHashTable(NXPtrPrototype, 0, NULL); NSDate *methodStart = [NSDate date]; for (NSInteger i = 0; i < capacity.integerValue; i++) { NSString *value = [NSString stringWithFormat:@"%ld", (long)i]; NXHashInsert(hashTable, (__bridge void *)value); } NSDate *methodFinish = [NSDate date]; NSTimeInterval executionTime = [methodFinish timeIntervalSinceDate:methodStart]; NSLog(@"Capacities: %@, executionTime = %f, meanTime = %.10f", capacity, executionTime, executionTime / capacity.integerValue); free(hashTable); } } return 0; } </code></pre> <p>代码中初始化了一个 <code>capacities</code> 存储需要测量的数据量级,然后调用 <code>NXHashInsert</code>方法将相当数量级的数据添加到哈希表中:</p> <p>|Capacities|Execution Time| Mean Time| |-------:|---------:|-------------:| | 100| 0.000334| 0.0000033402 | | 1000| 0.001962| 0.0000019619 | | 10000| 0.022001| 0.0000022001 | | 100000| 0.349998| 0.0000035000 | | 1000000| 2.622551| 0.0000026226 | | 2000000| 4.165023| 0.0000020825 | | 3000000| 6.973098| 0.0000023244 | | 5000000| 13.179743| 0.0000026359 | |10000000| 53.387356| 0.0000053387 |</p> <p>在对 <code>NXHashTable</code> 的性能测试中,当数据量小于 5000000 时,执行时间的增长还是线性的,平均时间也基本稳定,但是一旦数据量达到了千万级,执行时间就会出现显著的增长。</p> <p>如果仅仅在哈希表中插入数据,相信其时间增长应该都是线性的,这里出现问题的原因推测是在对哈希表进行 Rehash 的时候,迁移原数据至新的数组所造成的。</p> <p>如何避免哈希表的 Rehash 呢,重新回顾一下创建哈希表的函数:</p> <pre> <code class="language-objectivec">NXHashTable *NXCreateHashTable (NXHashTablePrototype prototype, unsigned capacity, const void *info); </code></pre> <p>这个函数的签名中包含一个 <code>capacity</code> 的参数,我们在上面的代码中传入了 0,也就是最开始的 <code>buckets</code> 数为 0,但是它的数目并不是固定的,它会随着哈希表中数据的增多,逐渐变大。</p> <blockquote> <p><code>capacity</code> 只是一个提示,帮助 NXHashTable 了解其中会存储多少数据。</p> </blockquote> <p>如果在创建 <code>NXHashTable</code> 时传入 <code>capacity.integerValue</code>:</p> <pre> <code class="language-objectivec"> NXHashTable *hashTable = NXCreateHashTable(NXPtrPrototype, capacity.integerValue, NULL); </code></pre> <p>重新运行代码,测量性能:</p> <p>|Capacities|Execution Time| Mean Time| |-------:|---------:|-------------:| | 100| 0.000740| 0.0000073999 | | 1000| 0.003442| 0.0000034420 | | 10000| 0.023341| 0.0000023341 | | 100000| 0.215209| 0.0000021521 | | 1000000| 1.836802| 0.0000018368 | | 2000000| 3.683246| 0.0000018416 | | 3000000| 5.474610| 0.0000018249 | | 5000000| 10.576254| 0.0000021153 | |10000000| 46.725459| 0.0000046725 |</p> <p>虽然在测试 <code>10,000,000</code> 数据时其平均时间依然是 <code>5,000,000</code> 时的二倍,不过整体的性能都有所提升,然而这部分性能的损耗暂时还不是很清楚原因。</p> <p>如果我们使用 Instrument 对有无 <code>capacity</code> 的情况进行比较(这是在使用 <code>2,000,000</code> 数据时进行的测试):</p> <p><img alt="上古时代 Objective-C 中哈希表的实现" src="https://simg.open-open.com/show/125944ee4ecd4f79ce96b462a701e0d9.jpg" width="712" height="527"></p> <p>没有传入 <code>capacity</code> 的哈希表会在多次插入之后出现一个峰值(由于 Rehash 引起的,其宽度就是 Rehash 使用的时间),而传入 <code>capacity</code> 的哈希表会在代码刚运行时就初始化足够大的数组。</p> <h2>NSMutableArray 性能</h2> <blockquote> <p>这部分只算是一个小插曲,你可以选择跳过这一小节的内容。</p> </blockquote> <p><code>NSMutableArray</code> 的构造器</p> <p><code>- (instancetype)initWithCapacity:(NSUInteger)numItems</code> 也有一个参数 <code>capacity</code>,虽然数组和哈希表是两种数据结构。</p> <p> </p> <blockquote> <p>不过我们这里主要研究的是:传入 <code>capacity</code> 是否会对性能造成影响。</p> </blockquote> <p>首先是使用 <code>init</code> 创建的 <code>NSMutableArray</code> 数组,也就是没有传入 <code>capacity</code>:</p> <p>|Capacities|Execution Time| Mean Time| |--------:|---------:|-------------:| | 100| 0.000539| 0.0000053900| | 1000| 0.003185| 0.0000031850| | 10000| 0.074033| 0.0000074033| | 100000| 0.370899| 0.0000037090| | 1000000| 1.504855| 0.0000015049| | 2000000| 2.852519| 0.0000014263| | 3000000| 3.995536| 0.0000013318| | 5000000| 6.833879| 0.0000013668| | 10000000| 14.444605| 0.0000014445|</p> <p>下面是使用 <code>initWithCapacity:</code> 创建的数组:</p> <p>|Capacities|Execution Time| Mean Time| |--------:|---------:|-------------:| | 100| 0.000256| 0.0000025600| | 1000| 0.001775| 0.0000017750| | 10000| 0.015906| 0.0000015906| | 100000| 0.174376| 0.0000017438| | 1000000| 1.650481| 0.0000016505| | 2000000| 2.802310| 0.0000014012| | 3000000| 4.451261| 0.0000014838| | 5000000| 7.093753| 0.0000014188| | 10000000| 14.598415| 0.0000014598|</p> <p>你可以在表格中看到,两者在执行效率上并没有显著的差异或者区别。</p> <p>但是如果使用 instrument 来查看两者的内存分配,可以很明显的看到,没有传入 <code>capacity</code> 的 <code>NSMutableArray</code> 会在可变数组内存占用增加前出现一个短暂的内存分配峰值。</p> <p><img alt="上古时代 Objective-C 中哈希表的实现" src="https://simg.open-open.com/show/e9dd9ffc4271ea6b407ee8241a25b0cd.jpg" width="1034" height="723"></p> <p>导致这一现象的原始可能是:在将原数组中的内容移入新数组时,临时变量申请了大量的内存控件。</p> <blockquote> <p>在之后关于 CoreFoundation 源代码分析的文中会介绍它们是怎么实现的。</p> </blockquote> <h2>NXHashTable 的应用</h2> <p>在整个 objc/runtime 中,作为私有的数据结构 <code>NXHashTable</code>,直接使用了它的就是存储所有类或者元类的哈希表(在这里会忽略对元类的存储,因为实现几乎完全相同):</p> <pre> <code class="language-objectivec">static NXHashTable *realized_class_hash = nil; </code></pre> <p>我么可以使用 <code>objc_copyClassList</code> 获取类的数组:</p> <pre> <code class="language-objectivec">Class * objc_copyClassList(unsigned int *outCount) { rwlock_writer_t lock(runtimeLock); realizeAllClasses(); Class *result = nil; NXHashTable *classes = realizedClasses(); unsigned int count = NXCountHashTable(classes); if (count > 0) { Class cls; NXHashState state = NXInitHashState(classes); result = (Class *)malloc((1+count) * sizeof(Class)); count = 0; while (NXNextHashState(classes, &state, (void **)&cls)) { result[count++] = cls; } result[count] = nil; } if (outCount) *outCount = count; return result; } </code></pre> <ol> <li>调用 <code>realizedClasses</code> 返回 <code>realized_class_hash</code> 哈希表</li> <li>使用 <code>NSHashState</code> 遍历 <code>realized_class_hash</code> 中的类,并将所有的类存入 <code>result</code></li> </ol> <p>接下来使用上面的方法,打印出 <code>realized_class_hash</code> 中存储的所有类:</p> <p><img alt="上古时代 Objective-C 中哈希表的实现" src="https://simg.open-open.com/show/bb66798f856bb298bdcf24fb04e500ae.jpg" width="1306" height="929"></p> <h2>小结</h2> <blockquote> <p><code>NXHashTable</code> 在 OS X 10.1 中就已经标记为弃用了,但是依旧支持着 runtime 底层的工作。</p> </blockquote> <p><code>NXHashTable</code> 可以说有着非常非常久远的历史了,最早可以追溯到将近 30 多年前 NeXT 时代:</p> <pre> <code class="language-objectivec">// hashtable2.mm 文件中 hashtable2.m Copyright 1989-1996 NeXT Software, Inc. Created by Bertrand Serlet, Feb 89 </code></pre> <p><code>NSHashTable</code> 对哈希表的实现还是非常优雅的,可以说非常标准的使用了<a href="/misc/goto?guid=4959672782601271013">拉链法</a>实现哈希表。</p> <p>不过现在,我们会使用 <code>NSHashTable</code> 来取代这个上古时代的产物。</p> <p>来自:<a href="/misc/goto?guid=4959672782784971021">http://draveness.me/hashtable/</a></p>