这段代码在性能方面有什么问题? List.Contains,随机使用,线程?

时间:2009-03-18 15:47:02

标签: c# multithreading loops list performance

我有一个本地类,其中有一个用于构建字符串列表的方法,我发现当我按下这个方法时(在一个1000次的for循环中),它通常不会返回我请求的数量。

我有一个全局变量:

string[] cachedKeys

传递给方法的参数:

int requestedNumberToGet

该方法与此类似:

List<string> keysToReturn = new List<string>();
int numberPossibleToGet = (cachedKeys.Length <= requestedNumberToGet) ? 
cachedKeys.Length : requestedNumberToGet;
Random rand = new Random();

DateTime breakoutTime = DateTime.Now.AddMilliseconds(5);

//Do we have enough to fill the request within the time? otherwise give 
//however many we currently have
while (DateTime.Now < breakoutTime
    && keysToReturn.Count < numberPossibleToGet
    && cachedKeys.Length >= numberPossibleToGet)
{
    string randomKey = cachedKeys[rand.Next(0, cachedKeys.Length)];
    if (!keysToReturn.Contains(randomKey))
        keysToReturn.Add(randomKey);
}

if (keysToReturn.Count != numberPossibleToGet)
    Debugger.Break();

我在cachedKeys中有大约40个字符串,长度不超过15个字符。

我不是线程专家,所以我实际上只是在循环中调用此方法1000次并且始终在那里进行调试。

运行的机器是一个相当强大的桌面,所以我希望突破时间是真实的,实际上它在循环的任何一点随机断开(我已经看过20s,100s,200s,300s)。

任何人都有任何想法,我在这里错了吗?

编辑:仅限于.NET 2.0

编辑:突破的目的是如果方法执行时间太长,客户端(使用XML提要数据的多个Web服务器)将不必等待其他项目依赖项初始化,他们只会得到0结果。

编辑:我想发布效果统计数据

原始

  • '0.0042477465711424217323710136' - 10
  • '0.0479597267250446634977350473' - 100
  • '0.4721072091564710039963179678' - 1000

  • '0.0007076318358897569383818334'-10
  • '0.007256508857969378789762386' - 100
  • '0.0749829936486341141122684587' - 1000

Freddy Rios

  • '0.0003765841748043396576939248' - 10
  • '0.0046003053460705201359390649' - 100
  • '0.0417058592642360970458535931' - 1000

6 个答案:

答案 0 :(得分:8)

为什么不把列表的副本 - O(n) - 随机播放,也是O(n) - 然后返回已经请求的密钥数。实际上,shuffle只需要是O(nRequested)。继续使用未抽取位的开头交换列表中未抽取位的随机成员,然后将混洗位扩展为1(只是一个名义计数器)。

编辑:这里有一些代码可以将结果产生为IEnumerable<T>。请注意,它使用延迟执行,因此如果您在首次开始迭代结果之前更改传入的源,您将看到这些更改。获取第一个结果后,元素将被缓存。

static IEnumerable<T> TakeRandom<T>(IEnumerable<T> source,
                                    int sizeRequired,
                                    Random rng)
{
    List<T> list = new List<T>(source);

    sizeRequired = Math.Min(sizeRequired, list.Count);

    for (int i=0; i < sizeRequired; i++)
    {
        int index = rng.Next(list.Count-i);            
        T selected = list[i + index];
        list[i + index] = list[i];
        list[i] = selected;
        yield return selected;
    }
}

我们的想法是,在您获取n元素之后的任何时候,列表中的第一个n元素都将是这些元素 - 因此我们确保不会再次选择这些元素。然后从“其余”中选择一个随机元素,将其交换到正确的位置并产生它。

希望这会有所帮助。如果你正在使用C#3,你可能希望通过将“this”放在第一个参数前面来使它成为一个扩展方法。

答案 1 :(得分:4)

一些想法。

首先,您的keysToReturn列表可能会在每次循环中添加,对吧?您正在创建一个空列表,然后将每个新密钥添加到列表中。由于列表没有预先调整大小,因此每个添加都成为O(n)操作(see MSDN documentation)。要解决此问题,请尝试像这样预先调整列表大小。

int numberPossibleToGet = (cachedKeys.Length <= requestedNumberToGet) ? cachedKeys.Length : requestedNumberToGet;
List<string> keysToReturn = new List<string>(numberPossibleToGet);

其次,你的突破时间在Windows上是不现实的(好的,好的,不可能的)。我在Windows计时中读过的所有信息表明,你可能希望的最好的是10毫秒的分辨率,但实际上它更像是15-18毫秒。实际上,请尝试以下代码:

for (int iv = 0; iv < 10000; iv++) {
    Console.WriteLine( DateTime.Now.Millisecond.ToString() );
}

您将在输出中看到的是离散跳跃。这是我刚在机器上运行的示例输出。

13
...
13
28
...
28
44
...
44
59
...
59
75
...

毫秒值从13跳到28到44到59到75.在我的机器的DateTime.Now函数中,这大约是15-16毫秒的分辨率。此行为与您在C运行时ftime()调用中看到的一致。换句话说,它是Windows计时机制的系统特征。关键是,你不应该依赖一致的5毫秒突破时间,因为你不会得到它。

第三,我是否正确地假设突破时间是阻止主线程锁定?如果是这样,那么将你的函数产生到ThreadPool线程并让它运行完成无论花多长时间都很容易。然后,您的主线程可以对数据进行操作。

答案 2 :(得分:4)

主要问题是在随机场景中使用重试以确保获得唯一值。这很快就会失控,特别是如果请求的物品数量接近要获得的物品数量,即如果增加钥匙数量,您会发现问题较少,但可以避免。

以下方法通过保留剩余的键列表来实现。

List<string> GetSomeKeys(string[] cachedKeys, int requestedNumberToGet)
{
    int numberPossibleToGet = Math.Min(cachedKeys.Length, requestedNumberToGet);
    List<string> keysRemaining = new List<string>(cachedKeys);
    List<string> keysToReturn = new List<string>(numberPossibleToGet);
    Random rand = new Random();
    for (int i = 0; i < numberPossibleToGet; i++)
    {
        int randomIndex = rand.Next(keysRemaining.Count);
        keysToReturn.Add(keysRemaining[randomIndex]);
        keysRemaining.RemoveAt(randomIndex);
    }
    return keysToReturn;
}

您的版本需要超时,因为您可能会继续重试以获取较长时间的值。特别是当您想要检索整个列表时,在这种情况下,您几乎肯定会因依赖重试的版本而失败。

更新:上述效果优于以下变化:

List<string> GetSomeKeysSwapping(string[] cachedKeys, int requestedNumberToGet)
{
    int numberPossibleToGet = Math.Min(cachedKeys.Length, requestedNumberToGet);
    List<string> keys = new List<string>(cachedKeys);
    List<string> keysToReturn = new List<string>(numberPossibleToGet);
    Random rand = new Random();
    for (int i = 0; i < numberPossibleToGet; i++)
    {
        int index = rand.Next(numberPossibleToGet - i) + i;
        keysToReturn.Add(keys[index]);
        keys[index] = keys[i];
    }
    return keysToReturn;
}
List<string> GetSomeKeysEnumerable(string[] cachedKeys, int requestedNumberToGet)
{
    Random rand = new Random();
    return TakeRandom(cachedKeys, requestedNumberToGet, rand).ToList();
}

一些具有10.000次迭代的数字:

Function Name    Elapsed Inclusive Time Number of Calls
GetSomeKeys              6,190.66       10,000
GetSomeKeysEnumerable     15,617.04       10,000
GetSomeKeysSwapping        8,293.64       10,000

答案 3 :(得分:2)

使用HashSet代替HashSet查找要比List

快得多
HashSet<string> keysToReturn = new HashSet<string>();
int numberPossibleToGet = (cachedKeys.Length <= requestedNumberToGet) ? cachedKeys.Length : requestedNumberToGet;
Random rand = new Random();

DateTime breakoutTime = DateTime.Now.AddMilliseconds(5);
int length = cachedKeys.Length;

while (DateTime.Now < breakoutTime && keysToReturn.Count < numberPossibleToGet) {
    int i = rand.Next(0, length);
    while (!keysToReturn.Add(cachedKeys[i])) {
        i++;
        if (i == length)
            i = 0;
    }
}

答案 4 :(得分:1)

考虑使用Stopwatch代替DateTime.Now。当你谈论毫秒时,它可能只是DateTime.Now的不准确性。

答案 5 :(得分:0)

问题可能就在这里:

if (!keysToReturn.Contains(randomKey))
    keysToReturn.Add(randomKey);

这将需要遍历列表以确定密钥是否在返回列表中。但是,可以肯定的是,您应该尝试使用工具进行分析。此外,5毫秒非常快.005秒,你可能想要增加它。