如何在C#中快速获取字符串中char的出现次数?

时间:2017-06-14 16:30:44

标签: c# .net

我有一个txt文件。现在,我需要逐行加载它,并检查整个文件中“@”的次数。

所以,基本上,我有一个单行字符串,如何快速获得'@'的出现次数?

我需要快速计算,因为我们有很多像这样的文件,每个文件大约300-400MB。

我搜索过,似乎直截了当的方式是最快的方法:

int num = 0;
foreach (char c in line)
{
    if (c == '@') num++;
}

有没有比这更快的方法?还有其他建议吗?

  • 如果需要,我们不必逐行加载txt文件,但我们需要知道每个文件中的#行。

由于

3 个答案:

答案 0 :(得分:4)

最快的方法实际上与I / O功能和计算速度有关。通常,了解什么是最快技术的最佳方法是对它们进行基准测试。

免责声明:结果(当然)绑定到我的机器,并且可能在不同的硬件上有很大差异。为了测试,我使用了大约400MB的单个文本文件。如果感兴趣,可以下载文件here(压缩)。可执行文件编译为x86。

选项1:读取整个文件,不进行并行化

long count = 0;

var text = File.ReadAllText("C:\\tmp\\test.txt");
for(var i = 0; i < text.Length; i++)
if (text[i] == '@')
    count++;

结果:

  • 平均执行时间:5828 ms
  • 平均进程记忆:1674 MB

这是“天真”方法,它读取内存中的整个文件,然后使用for循环(明显快于foreach或LINQ)。

正如预期的那样,进程占用的内存非常高(大约是文件大小的4倍),这可能是由内存中的字符串大小(更多信息here)和字符串处理开销的组合引起的。

选项2:以块的形式读取文件,不进行并行化

long count = 0;
using(var file = File.OpenRead("C:\\tmp\\test.txt"))
using(var reader = new StreamReader(file))
{
    const int size = 500000; // chunk size 500k chars
    char[] buffer = new char[size];

    while(!reader.EndOfStream)
    {
        var read = await reader.ReadBlockAsync(buffer, 0, size); // read chunk

        for(var i = 0; i < read; i++)
        if(buffer[i] == '@')
            count++;
    }
}

结果:

  • 平均执行时间:4819 ms
  • 平均进程记忆:7.48 MB

这是出乎意料的。在这个版本中,我们以500k字符的块来读取文件,而不是将其完全加载到内存中,并且执行时间甚至比前一种方法更低。请注意,减少块大小会增加执行时间(因为开销)。内存消耗极低(正如预期的那样,我们只将大约500kB / 1MB的内存直接加载到char数组中)。

通过更改块大小可以获得更好(或更差)的性能。

选项3:以块的形式读取文件,使用并行化

long count = 0;
using(var file = File.OpenRead("C:\\tmp\\test.txt"))
using(var reader = new StreamReader(file))
{
    const int size = 2000000; // this is roughly 4 times the single threaded value
    const int parallelization = 4; // this will split chunks in sub-chunks processed in parallel
    char[] buffer = new char[size];

    while(!reader.EndOfStream)
    {
        var read = await reader.ReadBlockAsync(buffer, 0, size);

        var sliceSize = read/parallelization;
        var counts = new long[parallelization];

        Parallel.For(0, parallelization, i => {
            var start = i * sliceSize;
            var end = start + sliceSize;

            if(i == parallelization)
                end += read % parallelization;

            long localCount = 0;
            for(var j = start; j < end; j++)
            {
                if(buffer[(int)j] == '@')
                    localCount++;
            }
            counts[i] = localCount;
        });

        count += counts.Sum();
    }
}

结果:

  • 平均执行时间:3363 ms
  • 平均进程记忆:10.37 MB

正如预期的那样,这个版本的单线程版本表现更好,但不如我们想象的那么好4倍。与第一个版本相比,内存消耗再次非常低(与之前相同),我们正在利用多核环境。

块大小和并行任务数等参数可能会显着改变结果,您应该通过反复试验来找到最适合您的组合。

结论

我倾向于认为“加载内存中的所有内容”版本是最快的,但这实际上取决于字符串处理和I / O速度的开销。并行分块的方法似乎是我机器中最快的,这应该引导你一个想法:当有疑问时,只需对它进行基准测试。

答案 1 :(得分:1)

您可以测试它是否更快,但更短的编写方式是:

int num = File.ReadAllText(filePath).Count(i => i == '@');
嗯,但我刚看到你也需要线数,所以这是类似的。同样,需要与你拥有的东西进行比较:

var fileLines = File.ReadAllLines(filePath);
var count = fileLines.Length();
var num = fileLines.Sum(line => line.Count(i => i == '@'));

答案 2 :(得分:-2)

你可以使用指针。我不知道这会更快。你必须做一些测试:

static void Main(string[] args)
{
    string str = "This is @ my st@ing";
    int numberOfCharacters = 0;

    unsafe
    {
        fixed (char *p = str)
        {
            char *ptr = p;
            while (*ptr != '\0')
            {
                if (*ptr == '@')
                    numberOfCharacters++;
                ptr++;
            }
        }
    }

    Console.WriteLine(numberOfCharacters);
}

请注意,您必须进入项目属性并允许不安全的代码才能使此代码正常工作。