如何使我的函数像ArrayList上的“Contains”一样快速运行?

时间:2010-02-25 20:31:55

标签: c# arraylist contains

我无法弄清楚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次。我得到的时间是136和 我的函数133毫秒,87版本的Contains远程获胜者。那么现在,如果你之前认为数据稀缺并且我将我的结论建立在第一次孤立的运行之前,你对这个测试有什么看法?并非只有平均Contains表现更好,但它在每次运行中都能获得始终如一的更好结果。那么,第三方功能在这里有什么不利之处,或者是什么?

11 个答案:

答案 0 :(得分:11)

首先,你没有多次运行它并比较平均值。

其次,你的方法在实际运行之前不会被jitted。因此,及时编译时间会被添加到执行时间中。

真正的测试会多次运行并对结果取平均值(任何数量的事情都可能导致运行X中的一个或另一个在总Y中变慢),并且应该使用{预先装配程序集{ {3}}

答案 1 :(得分:5)

当您使用.NET 3.5时,为什么使用ArrayList开始而不是List<string>

要尝试的一些事项:

  • 您可以看到使用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<T>并重复使用它。

答案 2 :(得分:2)

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

我能看到的唯一区别是调用list[i]确实限制了对i的检查,而Contains方法却没有。{/ p>

答案 3 :(得分:1)

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

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

答案 4 :(得分:1)

首先,如果您使用的是提前知道的类型,我建议使用泛型。所以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编译时,因为框架显然已经被编译了。

答案 5 :(得分:1)

使用下面的代码,我能够相对稳定地获得以下时间(在几毫秒内):
 1:190ms DoesContainRev
 2:198ms DoesContainRev1
 3:188ms DoesContainFwd
 4:203ms DoesContainFwd1
 5:199ms包含

这里有几点需要注意。

  1. 这是使用命令行中的发布编译代码运行的。许多人犯了在Visual Studio调试环境中对代码进行基准测试的错误,更不用说这里的任何人都做了但需要注意的事情。

  2. 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;
    }
             }
            }
    

答案 6 :(得分:1)

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

要查看,请将您的游戏更改为此表单:

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

我刚刚移动了一些线条。 JIT问题对于这一次重复是无关紧要的。

答案 7 :(得分:0)

阅读评论后修改:

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

答案 8 :(得分:0)

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

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

答案 9 :(得分:0)

使用SortedList<TKey,TValue>Dictionary<TKey, TValue>System.Collections.ObjectModel.KeyedCollection<TKey, TValue>根据密钥进行快速访问。

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<TKey, TValue>也很快并允许索引查找,但是,它需要继承,因为它是抽象的。因此,您需要一个特定的集合。但是,通过以下内容,您可以创建通用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方法不需要指定密钥。

答案 10 :(得分:0)

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