如何使我的函数在 ArrayList 上运行得与“包含”一样快?

Posted

技术标签:

【中文标题】如何使我的函数在 ArrayList 上运行得与“包含”一样快?【英文标题】:How can I make my function run as fast as "Contains" on an ArrayList? 【发布时间】:2011-01-21 04:46:03 【问题描述】:

我无法弄清楚Contains 方法在ArrayList 中查找元素所需的时间与我编写的用于执行相同操作的小函数所需的时间之间的差异。该文档指出Contains 执行线性搜索,因此它应该在O(n) 中,而不是任何其他更快的方法。但是,虽然确切的值可能不相关,但 Contains 方法会在 00:00:00.1087087 秒内返回,而我的函数需要 00:00:00.1876165。它可能不多,但是在处理更大的数组时,这种差异会变得更加明显。我缺少什么,我应该如何编写函数来匹配Contains 的表现?

我在 .NET 3.5 上使用 C#。

public partial class Window1 : Window

    public bool DoesContain(ArrayList list, object element)
    
        for (int i = 0; i < list.Count; i++)
            if (list[i].Equals(element)) return true;

        return false;
    

    public Window1()
    
        InitializeComponent();

        ArrayList list = new ArrayList();
        for (int i = 0; i < 10000000; i++) list.Add("zzz " + i);

        Stopwatch sw = new Stopwatch();
        sw.Start();

        //Console.Out.WriteLine(list.Contains("zzz 9000000") + " " + sw.Elapsed);
        Console.Out.WriteLine(DoesContain(list, "zzz 9000000") + " " + sw.Elapsed);
    

编辑:

好的,现在,小伙子们,看:

public partial class Window1 : Window

    public bool DoesContain(ArrayList list, object element)
    
        int count = list.Count;
        for (int i = count - 1; i >= 0; i--)
            if (element.Equals(list[i])) return true;

        return false;
    


    public bool DoesContain1(ArrayList list, object element)
    
        int count = list.Count;
        for (int i = 0; i < count; i++)
            if (element.Equals(list[i])) return true;

        return false;
    

    public Window1()
    
        InitializeComponent();

        ArrayList list = new ArrayList();
        for (int i = 0; i < 10000000; i++) list.Add("zzz " + i);

        Stopwatch sw = new Stopwatch();
        long total = 0;
        int nr = 100;

        for (int i = 0; i < nr; i++)
        
            sw.Reset();
            sw.Start();
            DoesContain(list,"zzz");
            total += sw.ElapsedMilliseconds;
        
        Console.Out.WriteLine(total / nr);


        total = 0;
        for (int i = 0; i < nr; i++)
        
            sw.Reset();
            sw.Start();
            DoesContain1(list, "zzz");
            total += sw.ElapsedMilliseconds;
        
        Console.Out.WriteLine(total / nr);


        total = 0;
        for (int i = 0; i < nr; i++)
        
            sw.Reset();
            sw.Start();
            list.Contains("zzz");
            total += sw.ElapsedMilliseconds;
        
        Console.Out.WriteLine(total / nr);
    
  

我的函数的两个版本(前向和后向循环)和默认的 Contains 函数平均运行了 100 次。我得到的时间是136133 毫秒我的函数和 87 的遥远赢家 Contains 版本。好吧,如果在您争辩说数据稀缺之前,我的结论是基于第一次独立运行的,您对这个测试有什么看法? Contains 不仅平均表现更好,而且在每次运行中都能获得始终如一的更好结果。那么,这里对于 3rd 方功能是否存在某种劣势,还是什么?

【问题讨论】:

您不应该在 .NET 3.5 上使用 ArrayList。如果你的集合只包含字符串,你应该使用List&lt;string&gt;。此外,为了获得更准确的读数,您应该多次执行代码,有很多事情会干扰这段代码,例如 JIT 时间。 要注意的另一件事是,由于您正在处理对象,ReferenceEquals 可能比 Equals 更快。这仅在您想查找它是否包含该特定对象而不是相同值时才有效。 致所有提供替代结构的人,你在回答谁?我对大量的答案感到困惑...... @leppie 我很确定他的意思是打扰的意思是它打断了他,而不是它深深地困扰着他。 @Henk Holterman:不完全是,Elapsed 会在Console.WriteLine 之前调用,是否在DoesContains 调用之后调用是另一回事。不保证执行顺序。 【参考方案1】:

当您使用 .NET 3.5 时,为什么要使用 ArrayList 而不是 List&lt;string&gt;

有几件事可以尝试:

您可以查看使用 foreach 代替 for 循环是否有帮助

你可以缓存计数:

public bool DoesContain(ArrayList list, object element)

    int count = list.Count;
    for (int i = 0; i < count; i++)
    
        if (list[i].Equals(element))
        
            return true;
        
        return false;
    

你可以颠倒比较:

if (element.Equals(list[i]))

虽然我不期望其中任何一个会产生显着(积极)的影响,但它们是我接下来要尝试的事情。

您是否需要多次进行此收容测试?如果是这样,您可能想要构建一个 HashSet&lt;T&gt; 并重复使用它。

【讨论】:

或者只是Equals(element, list[i]) :) 缓存Count 与边界检查相比是否适得其反?我的印象是,如果 JIT 识别出一个范围内的循环,它会忽略这些。如果它开始内联Count,嘿,我们达成了协议。 我怀疑使用foreach 会产生最大的影响,因为这样你就永远不会得到计数,并且避免了每次循环计算list[i] 的成本。跨度> 使用 foreach 确实有最大的影响。负面影响,就是这样。它慢了一倍。但是,缓存 Count 并反转比较确实有一点帮助,但它仍然没有考虑到一小部分差异。 Foreach 对于原始数组来说速度很快。对于数组列表,额外的间接层是一个杀手。【参考方案2】:

阅读cmets后修改:

它不使用一些Hash-alogorithm 来启用快速查找。

【讨论】:

不,它没有 - 正如“文档说明 Contains 执行线性搜索”所示 根据文档,它正在使用ArrayList 中每个元素的EqualsCompareTo 方法执行线性搜索。【参考方案3】:

我不确定你是否被允许发布 Reflector 代码,但是如果你使用 Reflector 打开方法,你可以看到它本质上是一样的(对空值有一些优化,但是你的测试线束不包括空值)。

我能看到的唯一区别是调用 list[i] 会对 i 进行边界检查,而 Contains 方法则不会。

【讨论】:

【参考方案4】:

我的猜测是 ArrayList 是用 C++ 编写的,并且可能会利用一些微优化(注意:这是一个猜测)。

例如,在 C++ 中,您可以使用指针算法(特别是递增指针以迭代数组)比使用索引更快。

【讨论】:

您可以使用reflector 来反编译 ArrayList 并查看它的全部源代码。它不是用 C++ 编写的,而是用常规的基于 CLR 的语言编写的。我的猜测是 C# 的早期版本。【参考方案5】:

使用 真正 好的优化器根本不应该有区别,因为语义似乎是相同的。然而现有的优化器可以优化你的功能不如硬编码的Contains优化。一些优化点:

    每次与属性比较可能比向下计数和与 0 比较要慢 函数调用本身有其性能损失 使用迭代器而不是显式索引可以更快(foreach 循环而不是普通的 for

【讨论】:

看起来像向下计数并与 0 比较就可以了。我的功能时间已降至 00:00:00.1416727。谢谢! 好朋友!坦率地说,我没想到这种老式的优化仍然有效。【参考方案6】:

首先,您不会多次运行它并比较平均值。

其次,您的方法在实际运行之前不会被触发。所以及时编译时间被添加到它的执行时间中。

一个真正的测试会运行多次并对结果进行平均(任何数量的事情都可能导致运行 X 超过 Y 的速度),并且你的程序集应该使用 @ 987654321@.

【讨论】:

其实,我会说这是正确的答案。实际的实现几乎没有什么不同。 +1 用于多次运行。如果是 187 秒和 108 秒之间的差值,也许少跑就可以了;但相差 79 毫秒,您需要更多的数据点才能得出可靠的结论。【参考方案7】:

使用SortedList&lt;TKey,TValue&gt;Dictionary&lt;TKey, TValue&gt;System.Collections.ObjectModel.KeyedCollection&lt;TKey, TValue&gt; 基于密钥进行快速访问。

var list = new List<myObject>(); // Search is sequential
var dictionary = new Dictionary<myObject, myObject>(); // key based lookup, but no sequential lookup, Contains fast
var sortedList = new SortedList<myObject, myObject>(); // key based and sequential lookup, Contains fast

KeyedCollection&lt;TKey, TValue&gt; 也很快并且允许索引查找,但是它需要被继承,因为它是抽象的。因此,您需要一个特定的集合。但是,您可以通过以下方式创建一个通用的KeyedCollection

public class GenericKeyedCollection<TKey, TValue> : KeyedCollection<TKey, TValue> 
   public GenericKeyedCollection(Func<TValue, TKey> keyExtractor) 
      this.keyExtractor = keyExtractor;
   

   private Func<TValue, TKey> keyExtractor;

   protected override TKey GetKeyForItem(TValue value) 
      return this.keyExtractor(value);
   

使用 KeyedCollection 的优点是 Add 方法不需要指定键。

【讨论】:

【参考方案8】:

首先,如果您使用的是提前知道的类型,我建议您使用泛型。所以 List 而不是 ArrayList。在幕后,ArrayList.Contains 实际上比您所做的要多一点。以下来自反射器:

public virtual bool Contains(object item)

    if (item == null)
    
        for (int j = 0; j < this._size; j++)
        
            if (this._items[j] == null)
            
                return true;
            
        
        return false;
    
    for (int i = 0; i < this._size; i++)
    
        if ((this._items[i] != null) && this._items[i].Equals(item))
        
            return true;
        
    
    return false;

请注意,它会在传递给 item 的 null 值时分叉自己。但是,由于您的示例中的所有值都不是 null,因此在开始和第二个循环中对 null 的额外检查理论上应该花费更长的时间。

您确定您正在处理完全编译的代码吗?即,当您的代码第一次运行时,它会被 JIT 编译,因为显然已经编译了框架。

【讨论】:

他也在调用 ArrayList.Item[Int32],它会进行两次整数检查 if ((index &lt; 0) || (index &gt;= this._size))【参考方案9】:

使用数组结构,在没有任何附加信息的情况下,您无法比 O(n) 更快地进行搜索。 如果您知道数组已排序,那么您可以使用二进制搜索算法并且只花费 o(log(n)) 否则你应该使用一个集合。

【讨论】:

对不起,我的帖子不适用于该问题【参考方案10】:

使用下面的代码,我能够相对一致地获得以下时间(在几毫秒内): 1: 190ms DoesContainRev 2:198ms 包含Rev1 3: 188 毫秒是否包含前移 4: 203ms 包含Fwd1 5:199ms 包含

这里有几点需要注意。

    这是从命令行使用发布编译代码运行的。许多人在 Visual Studio 调试环境中对代码进行基准测试时会犯错误,并不是说这里有人做过,而是要小心。

    list[i].Equals(element) 似乎比element.Equals(list[i]) 慢一点。

    using System;
    using System.Diagnostics;
    using System.Collections;
    
    
    namespace ArrayListBenchmark
    
    class Program
    
        static void Main(string[] args)
        
            Stopwatch sw = new Stopwatch();
            const int arrayCount = 10000000;
            ArrayList list = new ArrayList(arrayCount);
            for (int i = 0; i < arrayCount; i++) list.Add("zzz " + i);
        sw.Start();
        DoesContainRev(list, "zzz");
        sw.Stop();
        Console.WriteLine(String.Format("1: 0", sw.ElapsedMilliseconds));
        sw.Reset();
    
        sw.Start();
        DoesContainRev1(list, "zzz");
        sw.Stop();
        Console.WriteLine(String.Format("2: 0", sw.ElapsedMilliseconds));
        sw.Reset();
    
        sw.Start();
        DoesContainFwd(list, "zzz");
        sw.Stop();
        Console.WriteLine(String.Format("3: 0", sw.ElapsedMilliseconds));
        sw.Reset();
    
        sw.Start();
        DoesContainFwd1(list, "zzz");
        sw.Stop();
        Console.WriteLine(String.Format("4: 0", sw.ElapsedMilliseconds));
        sw.Reset();
    
        sw.Start();
        list.Contains("zzz");
        sw.Stop();
        Console.WriteLine(String.Format("5: 0", sw.ElapsedMilliseconds));
        sw.Reset();
    
        Console.ReadKey();
    
    public static bool DoesContainRev(ArrayList list, object element)
    
        int count = list.Count;
        for (int i = count - 1; i >= 0; i--)
            if (element.Equals(list[i])) return true;
    
        return false;
    
    public static bool DoesContainFwd(ArrayList list, object element)
    
        int count = list.Count;
        for (int i = 0; i < count; i++)
            if (element.Equals(list[i])) return true;
    
        return false;
    
    public static bool DoesContainRev1(ArrayList list, object element)
    
        int count = list.Count;
        for (int i = count - 1; i >= 0; i--)
            if (list[i].Equals(element)) return true;
    
        return false;
    
    public static bool DoesContainFwd1(ArrayList list, object element)
    
        int count = list.Count;
        for (int i = 0; i < count; i++)
            if (list[i].Equals(element)) return true;
    
        return false;
    
             
            
    

【讨论】:

for(int i = 0; i = 0; i--) 循环甚至无法编译? 是的,这是调试环境。 Count 我仍然有更好的时间,但差异可以忽略不计。 我不确定哪里有 for(int i = 0; i = 0; i--) 我相信我的示例中的所有代码都是正确的 (int i = count - 1; i &gt;= 0; i--) 抱歉,如果我错过了什么。【参考方案11】:

在您编辑之后,我复制了代码并对其进行了一些改进。 差异不可重现,结果证明是测量/舍入问题。

要看到这一点,请将您的运行更改为以下形式:

    sw.Reset();
    sw.Start();
    for (int i = 0; i < nr; i++)
              
        DoesContain(list,"zzz");            
    
    total += sw.ElapsedMilliseconds;
    Console.WriteLine(total / nr);

我只是移动了一些行。重复次数如此之多,JIT 问题就无关紧要了。

【讨论】:

以上是关于如何使我的函数在 ArrayList 上运行得与“包含”一样快?的主要内容,如果未能解决你的问题,请参考以下文章

如何让我的 CSS @media 代码在移动设备上运行以使我的网站具有响应性?

如何使我的程序与 DEP 兼容?

如何使我的自定义对象 Parcelable?

如何使我的网站对移动设备更敏感?

select 语句中的函数使我的查询运行非常缓慢

如何使我的本地服务器区分大小写?