如何实现无需comparison约束的Set.contains优化seq元素检查?
优化Seq.contains:针对Set类型的高效实现
在F#中,Seq.contains仅要求元素类型具备equality特性,而Set.contains还要求具备comparison特性。我们需要实现一个函数:当输入的seq<'a>实际是Set<'a>时,调用O(log n)的Set.contains;否则使用O(n)的Seq.contains,同时不强制要求'a具备comparison约束。
初始尝试的问题
直接类型匹配的写法会强制'a满足comparison约束,因为Set<'a>本身要求这个约束:
let setOptimizedContains item (xs: seq<'a>) = match xs with | :? Set<'a> as x -> x.Contains item | _ -> Seq.contains item xs
这段代码无法用于不具备comparison特性的类型,不符合需求。
基于反射+委托编译的优化方案
通过反射识别Set类型,并预编译Contains方法为委托缓存起来,既能保留Set场景的高性能,又不会给非Set场景带来过大开销:
open System open System.Collections.Generic open System.Reflection let private containsCache = Dictionary<Type, Func<obj, obj, bool>>() let private getSetContainsDelegate (setType: Type) = lock containsCache (fun () -> match containsCache.TryGetValue setType with | true, del -> del | false, _ -> let containsMethod = setType.GetMethod("Contains", BindingFlags.Public ||| BindingFlags.Instance) let del = Func<obj, obj, bool>(fun set item -> containsMethod.Invoke(set, [| item |]) :?> bool ) containsCache.[setType] <- del del ) let setOptimizedContains (item: 'a) (xs: seq<'a>) = let xsType = xs.GetType() if xsType.IsGenericType && xsType.GetGenericTypeDefinition() = typeof<Set<_>>.GetGenericTypeDefinition() then let del = getSetContainsDelegate xsType del.Invoke(xs, box item) else Seq.contains item xs
方案说明
- 缓存委托:首次遇到Set类型时,编译其
Contains方法为Func<obj, obj, bool>并缓存,后续调用直接使用委托,避免重复反射开销。 - 类型检查:仅在首次调用时检查输入序列的类型,非Set场景直接走
Seq.contains,性能接近原生实现。 - 无约束要求:通过反射绕开了编译时的
comparison约束,只要实际的Set类型满足该约束(它必然满足),运行时就能正常调用。
性能测试对比
| 方法 | N | 实现类型 | 平均耗时 | 误差 | 标准差 | 比值 | 比值标准差 | Gen0 | 内存分配 | 分配比值 |
|---|---|---|---|---|---|---|---|---|---|---|
| 默认实现 | 10 | 数组 | 38.61 ns | 0.296 ns | 0.231 ns | 1.00 | 0.00 | 0.0019 | 32 B | 1.00 |
| 优化实现 | 10 | 数组 | 232.47 ns | 4.100 ns | 3.835 ns | 5.99 | 0.09 | 0.0305 | 512 B | 16.00 |
| 默认实现 | 10 | Set | 160.08 ns | 1.574 ns | 1.472 ns | 1.00 | 0.00 | 0.0429 | 720 B | 1.00 |
| 优化实现 | 10 | Set | 48.22 ns | 0.392 ns | 0.367 ns | 0.30 | 0.00 | 0.0048 | 80 B | 0.11 |
| 默认实现 | 1000 | 数组 | 2,779.40 ns | 54.438 ns | 74.516 ns | 1.00 | 0.00 | - | 32 B | 1.00 |
| 优化实现 | 1000 | 数组 | 20,255.82 ns | 110.821 ns | 103.662 ns | 7.27 | 0.21 | 2.8687 | 48032 B | 1,501.00 |
| 默认实现 | 1000 | Set | 13,696.04 ns | 107.530 ns | 100.583 ns | 1.000 | 0.00 | 3.5858 | 60120 B | 1.000 |
| 优化实现 | 1000 | Set | 62.47 ns | 0.420 ns | 0.372 ns | 0.005 | 0.00 | 0.0048 | 80 B | 0.001 |
结果分析
- Set场景:优化实现的性能提升极为显著,耗时仅为默认实现的0.5%~30%,内存分配也大幅降低。
- 非Set场景:首次调用会有反射和委托编译的开销,但后续调用的开销会逐渐降低;如果你的代码中大部分序列不是Set,可以考虑进一步优化(比如增加快速路径判断)。
内容的提问来源于Stack Exchange,提问作者cmeeren
相关产品推荐
相关产品推荐

