我有一个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);
}
请注意,必须进入项目属性并允许不安全的代码才能使此代码正常工作。