令人惊讶的性能差异:List.Contains,SortedList.ContainsKey,DataRowCollection.Contains,DataTable.Select,DataTable.FindBy

6

最初我想询问查询Datatable中特定行的最快方法。

我已经测试了5种不同的方法,得出了一个令人惊讶的结果(对我来说)。

背景: 我在MS Sql-Server 2005数据库中创建了一个View。这个View目前有6318行。因为我必须经常检查给定的id是否存在于这个View中,所以我想知道最有效的方法是什么。我在一个强类型数据集中创建了一个DataAdapter,它返回所有行并填充一个Datatable。 我的第一种方法是创建一个共享的泛型List(of Int32),并在应用程序启动时将其填充到来自View的ID中。然后使用List.Contains检查当前ID是否在此列表中。由于所有行都是不同的,我想知道使用SortedList及其ContainsKey方法是否更快。 然后我还检查了直接访问Datatable的性能,包括其Select-Method、自动生成的(当列被定义为主键时)FindBy-method和最后一个DatarowCollection.Contains方法。 因此,我有5种方法来检查我的ID是否在该View(或映射的List/SortedList)中。

我使用System.Diagnostics.StopWatch测试了它们的性能,并得到了一些有趣的结果。 我认为SortedList.ContainsKey必须比List.Contains更快,因为它们是不同的和排序的,但事实相反。 但对我来说最令人惊讶的是DataRowCollection.Contains方法(我先前忘记了)远远是最快的。 它甚至比dataTable.FindBy方法快50倍。

  1. 这些差异是由什么引起的?
  2. 我是否忘记了更好的方法?
  3. 我的测量方法是否正确(我认为最好循环它们并取那些值)?
  4. 这些值是否可转移或依赖于Datatable / Collection的大小?
  5. 在我的更新(1000000次迭代)之后,ContainsKey是最快的。 这是因为我总是检查相同的ID还是一般情况下? 是否有一种没有Dictionary KeyValue-Pair开销的SortedList?

结果 [1000000次迭代*]

  • Timespan 1 = SortedList.ContainsKey = Ø 0.65634 [238.1095] ms
  • Timespan 2 = List.Contains = Ø 0.06802 [57045.37955] ms
  • Timespan 3 = DataTable.FindByIdData(auto-generated method) = Ø 0.31580 [1542.62345] ms
  • Timespan 4 = DataTable.Select = Ø 0.27790 [26029.39635] ms
  • Timespan 5 = DataRowCollection.Contains = Ø 0.00638 [1202.79735] ms

    1.)
    Timespan 1: 0,6913 ms
    Timespan 2: 0,1053 ms
    Timespan 3: 0,3279 ms
    Timespan 4: 0,1002 ms
    Timespan 5: 0,0056 ms
    
    2.)
    Timespan 1: 0,6405 ms
    Timespan 2: 0,0588 ms
    Timespan 3: 0,3112 ms
    Timespan 4: 0,3872 ms
    Timespan 5: 0,0067 ms
    
    3.)
    Timespan 1: 0,6502 ms
    Timespan 2: 0,0588 ms
    Timespan 3: 0,3092 ms
    Timespan 4: 0,1268 ms
    Timespan 5: 0,007 ms
    
    4.)
    Timespan 1: 0,6504 ms
    Timespan 2: 0,0586 ms
    Timespan 3: 0,3092 ms
    Timespan 4: 0,3893 ms
    Timespan 5: 0,0063 ms
    
    5.)
    Timespan 1: 0,6493 ms
    Timespan 2: 0,0586 ms
    Timespan 3: 0,3215 ms
    Timespan 4: 0,386 ms
    Timespan 5: 0,0063 ms
    
    
    
    Timespan 1: 0,6913 0,6405 0,6502 0,6504 0,6493 = Ø 0,65634
    Timespan 2: 0,1053 0,0588 0,0588 0,0586 0,0586 = Ø 0,06802
    Timespan 3: 0,3279 0,3112 0,3092 0,3092 0,3215 = Ø 0,31580
    Timespan 4: 0,1002 0,3872 0,1268 0,3893 0,3860 = Ø 0,27790
    Timespan 5: 0,0056 0,0067 0,0070 0,0063 0,0063 = Ø 0,00638
    

为了完整起见,以下是部分VB.Net源代码

Dim applies As Boolean
Dim clock As New System.Diagnostics.Stopwatch

clock.Start()
For i As Int32 = 1 To 1000000
    applies = sortedListAC17NextClaims.ContainsKey(myClaim.idData)
Next
clock.Stop()
Dim timeSpan1 As String = "Timespan 1: " & clock.Elapsed.TotalMilliseconds.ToString & " ms"

clock.Reset()
clock.Start()
For i As Int32 = 1 To 1000000
    applies = listAC17NextClaims.Contains(myClaim.idData)
Next
clock.Stop()
Dim timeSpan2 As String = "Timespan 2: " & clock.Elapsed.TotalMilliseconds.ToString & " ms"

clock.Reset()
clock.Start()
For i As Int32 = 1 To 1000000
    applies = Not MyDS.AC17NextClaims.FindByIdData(myClaim.idData) Is Nothing
Next
clock.Stop()
Dim timeSpan3 As String = "Timespan 3: " & clock.Elapsed.TotalMilliseconds.ToString & " ms"

clock.Reset()
clock.Start()
For i As Int32 = 1 To 1000000
    applies = MyDS.AC17NextClaims.Select("idData=" & myClaim.idData).Length > 0
Next
clock.Stop()
Dim timeSpan4 As String = "Timespan 4: " & clock.Elapsed.TotalMilliseconds.ToString & " ms"

clock.Reset()
clock.Start()
For i As Int32 = 1 To 1000000
    applies = MyDS.AC17NextClaims.Rows.Contains(myClaim.idData)
Next
clock.Stop()
Dim timeSpan5 As String = "Timespan 5: " & clock.Elapsed.TotalMilliseconds.ToString & " ms"
  • UPDATE: I have changed my results and the source above. In squared brackets are the values for 1000000 iterations. Now the result is completely different. The fastest method now is definitely is the ContainsKey of the SortedList.

  • UPDATE 2: I forgot the alternative to use List.BinarySearch. This seems to be fastest for me:

    clock.Start()
    For i As Int32 = 1 To 1000000
        applies = listAC17NextClaims.BinarySearch(myClaim.idData) > -1
    Next
    clock.Stop()
    

    needs only 219.1805 ms to perform 1000000 iterations and hence is the fastest without the overhead of a SortedList-KeyValue-Pair. I can use it without to sort the list because the DataAdapter filled the datatable with an Order By clause.


在表中使用600M个项目进行最后一次测试,并使用每种方法进行两次搜索,丢弃每个方法的第一次运行的时间。 - Will Dean
那会花费太多时间,而且不符合我的要求。 - Tim Schmelter
3个回答

5

我觉得你提供的工作量不足以获得有用的计时结果。你所有的时间都是亚毫秒级别的,几乎肯定只是噪音 - 缓存、jit、抢占等。

请确保集合足够大,需要运行数秒钟,或者至少在紧密循环中运行每个测试多次以达到数秒钟。


JIT已经覆盖了,他进行了5次运行,但是通常的做法是进行多次运行并从中计算平均时间。 - Adam Houldsworth
他在那里有小于70微秒的时间 - 我不认为这种计时技术(或Windows上的任何其他计时技术)的有用分辨率会降到那么低。 - Will Dean
这5次运行是否在同一个虚拟机实例中并不清楚。如果他重新执行问题中的代码,那么它每次都会进行JIT编译,对吧? - Phil Miller
1000000次迭代确实会有很大的差异。现在结果已经改变了ContainsKey方法(请看我的更新)。 - Tim Schmelter
1
正确 - 但别忘了这仅告诉你使用该方法搜索包含6K项的列表1000000次会更快,而不是在一次搜索包含6bn项的列表中它将是最快的方法。你可能主要测量算法的启动时间而不是其搜索性能。这就是为什么O(f(n))标记只是故事的一部分的真正原因 - 因为真实时间可能更好地近似为O(f(n)) + K,如果n很小,K可能会占主导地位。 - Will Dean

5
为什么不使用以 HashTable 作为基础数据结构的集合(Dictionary<TKey, TValue>HashSet<T>)?如果键没有冲突,HashTables 应该提供 O(1) 的查找时间,并且不需要“排序”的开销。
编辑:如果你只想存储键,应该使用 HashSet<T>,它在 .NET 3.5 及以上版本中可用。 从MSDN获取关于 SortedList:

SortedList 对象的操作往往比 Hashtable 对象的操作慢,因为需要排序。

为了针对.NET 2.0,你可以自己创建一个或者使用预先构建的库,例如Wintellect's Power Collections(你也可以直接使用源代码)。

排序的开销只有一次,因为 SortedList 是共享的。我的一个问题是,是否有比 SortedList 更好的集合/字典,因为它在 KeyValue-Pairs 方面存在内存开销。我不需要 KeyValue-Pair,因为视图中只有 ID。如果在检查 Contains 之前对通用列表进行排序,那么它是否更快,或者是否有另一种可以以非线性方式搜索的集合类型?在我的情况下,键将与值相同,这似乎是多余的。 - Tim Schmelter
@Tim - 我相当确定 SortedList 会使用二分查找来查找元素(这是 O(log(n)))。你可以使用一个仅存储键的 HashSet<T> 来代替,它仍然是 O(1) 的搜索时间。 - TheCloudlessSky
谢谢你的提示,但不幸的是我正在使用2.0框架,而HashSet是3.5的一部分。 - Tim Schmelter
我想知道为什么Array.BinarySearch(myList.ToArray(),myClaim.idData)在1000000次迭代中需要18404.573毫秒,比ContainsKey(1:77)慢,即使是DataTable.FindBy和DataRowCollection.Contains也是如此。 - Tim Schmelter
@Tim - 因为你每次都在调用 .ToArray(),这会非常耗费资源。另外,请参考我上面的编辑关于使用 .NET 2.0 的内容。老实说,你可以只使用带有额外内存开销的 Dictionary<TKey, TValue> - TheCloudlessSky
显示剩余2条评论

0

正如已经指出的那样,您的代码只运行一次操作。通常的策略是多次运行代码(比如说,进行3次搜索),以获得更大的数字(因此,如果3次搜索需要0.9秒,您可以说一次搜索需要0.3秒)。然后循环几次,以便您可以计算平均值(如果您愿意,还可以包括标准差来过滤掉异常结果),然后在此基础上再运行一次,而不关注记录时间,以便执行任何JIT编译。

此外,请在没有调试器附加的情况下以发布模式运行代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接