如何在 C# 中获取字符串 FAST 中字符出现的 #?

  • 本文关键字:字符 字符串 获取 FAST c# .net
  • 更新时间 :
  • 英文 :


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

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

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

我搜索了一下,似乎直接的方法就是最快的方法:

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

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

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

谢谢

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

免责声明:结果(当然)绑定到我的机器上,在不同的硬件上可能会有很大差异。为了进行测试,我使用了大约 400MB 的单个文本文件。如果有兴趣,可以在此处下载该文件(压缩)。编译为 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 倍),这可能是由内存中的字符串大小(更多信息在这里)和字符串处理开销的组合引起的。

选项 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 速度。并行分块方法似乎是我的机器中最快的方法,这应该会引导您产生一个想法:如有疑问,只需对其进行基准测试。

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

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 == '@'));

您可以使用指针。我不知道这是否会更快。您必须进行一些测试:

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 != '')
{
if (*ptr == '@')
numberOfCharacters++;
ptr++;
}
}
}
Console.WriteLine(numberOfCharacters);
}

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

最新更新