为什么我的对象需要很长时间才能创建

本文关键字:长时间 创建 我的 对象 为什么 | 更新日期: 2023-09-27 18:24:48

我正在编写扫描大块文本并对其进行一些基本统计的代码,如大小写字符数、标点符号数等。

最初我的代码是这样的:

    foreach (var character in stringToCount)
    {
        if (char.IsControl(character))
        {
            controlCount++;
        }
        if (char.IsDigit(character))
        {
            digitCount++;
        }
        if (char.IsLetter(character))
        {
            letterCount++;
        } //etc.
    }

然后我创建了一个像这样的新对象,它只需读取局部变量并将它们传递给构造函数:

    var result = new CharacterCountResult(controlCount, highSurrogatecount, lowSurrogateCount, whiteSpaceCount,
        symbolCount, punctuationCount, separatorCount, letterCount, digitCount, numberCount, letterAndDigitCount,
        lowercaseCount, upperCaseCount, tempDictionary);

然而,Code Review Stack Exchange上的一位用户指出,我可以做以下操作。太好了,我为自己节省了大量代码,这很好。

    var result = new CharacterCountResult(stringToCount.Count(char.IsControl),
        stringToCount.Count(char.IsHighSurrogate), stringToCount.Count(char.IsLowSurrogate),
        stringToCount.Count(char.IsWhiteSpace), stringToCount.Count(char.IsSymbol),
        stringToCount.Count(char.IsPunctuation), stringToCount.Count(char.IsSeparator),
        stringToCount.Count(char.IsLetter), stringToCount.Count(char.IsDigit),
        stringToCount.Count(char.IsNumber), stringToCount.Count(char.IsLetterOrDigit),
        stringToCount.Count(char.IsLower), stringToCount.Count(char.IsUpper), tempDictionary);

但是以第二种方式创建对象大约需要(在我的机器上)额外的~200ms

这怎么可能?虽然这似乎不是一个很大的额外时间,但当我让它运行处理文本时,它很快就会增加。

我应该采取什么不同的做法?

为什么我的对象需要很长时间才能创建

您使用的是方法组(语法糖隐藏lambda或委托),并对字符进行多次迭代,而您只需一次就可以完成(就像在原始代码中一样)。

我记得你之前的问题,我记得看到过使用方法group和string的建议。数(char.IsLetterOrDigit),然后想"是的,这看起来很漂亮,但不会表现得很好",所以看到你真的发现了这一点,真的很有趣。

如果性能很重要,我只会在没有委托周期的情况下进行,并使用一个带有单次传递的巨大循环,这是没有委托或多次迭代的传统方式,甚至可以通过组织逻辑来调整它,从而组织排除其他情况的任何情况,从而进行"懒惰评估"。例如,如果你知道一个字符是空白,那么不要检查数字或字母等。或者,如果你认为它是digitalOrAlpha,那么在该条件中包括数字和字母检查。

类似于:

foreach(var ch in string) {
   if(char.IsWhiteSpace(ch)) {
      ...
   }
   else {
      if(char.IsLetterOrDigit(ch)) {
         letterOrDigit++;
         if(char.IsDigit(ch)) digit++;
         if(char.IsLetter(ch)) letter++;
      }  
   }
}

如果你真的想进行微优化,那么写一个程序来预先计算所有选项,并发出一个巨大的switch语句来进行表查找。

switch(ch) {
   case 'A':
        isLetter++;
        isUpper++;
        isLetterOrDigit++;
        break;
   case 'a':
        isLetter++;
        isLower++;
        isLetterOrDigit++;
        break;
   case '!':
        isPunctuation++;
   ...
}

现在,如果你想变得真正疯狂,根据现实生活中出现的频率组织切换语句,并将最常见的字母放在"树"的顶部,以此类推。当然,如果你那么关心速度,这可能是普通C.的工作

但我偏离了你最初的问题

按照您以前的方式遍历文本一次,在遍历过程中增加所有计数器。在你的新方式中,你浏览文本13次(每次呼叫stringToCount.Count(一次),每次通过只更新一个计数器。

然而,这类问题是Parallel.ForEach的完美情况。你可以用多个线程遍历文本(确保你的增量是线程安全的),并更快地获得总数。

Parallel.ForEach(stringToCount, character =>
{
    if (char.IsControl(character))
    {
        //Interlocked.Increment gives you a thread safe ++
        Interlocked.Increment(ref controlCount);
    }
    if (char.IsDigit(character))
    {
        Interlocked.Increment(ref digitCount);
    }
    if (char.IsLetter(character))
    {
        Interlocked.Increment(ref letterCount);
    } //etc.
});
var result = new CharacterCountResult(controlCount, highSurrogatecount, lowSurrogateCount, whiteSpaceCount,
    symbolCount, punctuationCount, separatorCount, letterCount, digitCount, numberCount, letterAndDigitCount,
    lowercaseCount, upperCaseCount, tempDictionary);

它仍然遍历文本一次,但许多工人将同时遍历文本的各个部分。