使用并行在 C# 中进行文件搜索优化
File search optimisation in C# using Parallel
我有一个文件夹,里面有很多 CSV 文件,每个大约 3MB。
一个 CSV 的内容示例:
afkla890sdfa9f8sadfkljsdfjas98sdf098,-1dskjdl4kjff;
afkla890sdfa9f8sadfkljsdfjas98sdf099,-1kskjd11kjsj;
afkla890sdfa9f8sadfkljsdfjas98sdf100,-1asfjdl1kjgf;
etc...
现在我有一个用 C# 编写的控制台应用程序,它在每个 CSV 文件中搜索特定字符串。
那些要搜索的字符串在一个 txt 文件中。
搜索 txt 文件的示例:
-1gnmjdl5dghs
-17kn3mskjfj4
-1plo3nds3ddd
然后我调用方法在给定文件夹的所有文件中搜索每个搜索字符串:
private static object _lockObject = new object();
public static IEnumerable<string> SearchContentListInFiles(string searchFolder, List<string> searchList)
{
var result = new List<string>();
var files = Directory.EnumerateFiles(searchFolder);
Parallel.ForEach(files, (file) =>
{
var fileContent = File.ReadLines(file);
if (fileContent.Any(x => searchList.Any(y => x.ToLower().Contains(y))))
{
lock (_lockObject)
{
foreach (string searchFound in fileContent.Where(x => searchList.Any(y => x.ToLower().Contains(y))))
{
result.Add(searchFound);
}
}
}
});
return result;
}
现在的问题是,我能否以任何方式提高此操作的性能?
我有大约 100GB 的文件要搜索。
在 SSD 磁盘和良好的 i7 CPU.
上,使用大约 25 个搜索字符串搜索所有 ~30.000 个文件大约需要 1 小时
拥有更大的 CSV 文件或拥有更小的 CSV 文件会有不同吗?我只是希望这个搜索尽可能快。
更新
我已经尝试了你写的每一个建议,现在这对我来说是最好的(从 LINQ 中删除 ToLower 产生了最好的性能提升。从 1 小时开始的搜索时间现在是 16 分钟!):
public static IEnumerable<string> SearchContentListInFiles(string searchFolder, HashSet<string> searchList)
{
var result = new BlockingCollection<string>();
var files = Directory.EnumerateFiles(searchFolder);
Parallel.ForEach(files, (file) =>
{
var fileContent = File.ReadLines(file); //.Select(x => x.ToLower());
if (fileContent.Any(x => searchList.Any(y => x.Contains(y))))
{
foreach (string searchFound in fileContent.Where(x => searchList.Any(y => x.Contains(y))))
{
result.Add(searchFound);
}
}
});
return result;
}
此操作首先是磁盘绑定。磁盘绑定操作不会从多线程中受益。事实上,你要做的就是同时用大量冲突请求淹没磁盘控制器,像 NCQ 这样的功能必须再次被删除。
如果您先将所有文件加载到内存中,您的操作将是内存绑定。并且内存绑定操作也不会从多线程中受益(通常;它会在此处详细介绍 CPU 和内存架构)。
虽然一定数量的多任务在编程中是强制性的,但真正的多线程仅有助于CPU 绑定操作。那里看起来没有任何内容 CPU 绑定。所以多线程taht搜索(每个文件一个线程)不会让它更快。由于所有线程切换和同步开销,确实可能会使它变慢。
可能 Lucene 之类的东西可以提高性能:为什么不索引数据以便轻松搜索?
您将避免按顺序搜索数据。此外,您可以基于相同的数据对多个索引进行建模,以便能够以光速获得某些结果。
尝试:
对一行执行 .ToLower 一次,而不是对 searchList 中的每个元素执行 .ToLower。
对文件进行一次扫描,而不是通过任意位置扫描两次。获取列表,如果找到则添加锁。在您的示例中,您在搜索和添加时浪费了两次传递的时间并阻塞了所有线程。
如果您知道要查找的位置(在您的示例中您知道),您可以从位置扫描,而不是在所有字符串中扫描
使用生产者消费者模式例如使用:BlockingCollection<T>
,所以不需要使用锁
如果需要在字段中严格搜索,构建searchList的HashSet并执行searchHash.Contains(fieldValue)这将显着增加进程
所以这里有一个示例(未测试):
using(var searcher = new FilesSearcher(
searchFolder: "path",
searchList: toLookFor))
{
searcher.SearchContentListInFiles();
}
这里是搜索者:
public class FilesSearcher : IDisposable
{
private readonly BlockingCollection<string[]> filesInMemory;
private readonly string searchFolder;
private readonly string[] searchList;
public FilesSearcher(string searchFolder, string[] searchList)
{
// reader thread stores lines here
this.filesInMemory = new BlockingCollection<string[]>(
// limit count of files stored in memory, so if processing threads not so fast, reader will take a break and wait
boundedCapacity: 100);
this.searchFolder = searchFolder;
this.searchList = searchList;
}
public IEnumerable<string> SearchContentListInFiles()
{
// start read,
// we not need many threads here, probably 1 thread by 1 storage device is the optimum
var filesReaderTask = Task.Factory.StartNew(ReadFiles, TaskCreationOptions.LongRunning);
// at least one proccessing thread, because reader thread is IO bound
var taskCount = Math.Max(1, Environment.ProcessorCount - 1);
// start search threads
var tasks = Enumerable
.Range(0, taskCount)
.Select(x => Task<string[]>.Factory.StartNew(Search, TaskCreationOptions.LongRunning))
.ToArray();
// await for results
Task.WaitAll(tasks);
// combine results
return tasks
.SelectMany(t => t.Result)
.ToArray();
}
private string[] Search()
{
// if you always get unique results use list
var results = new List<string>();
//var results = new HashSet<string>();
foreach (var content in this.filesInMemory.GetConsumingEnumerable())
{
// one pass by a file
var currentFileMatches = content
.Where(sourceLine =>
{
// to lower one time for a line, and we don't need to make lowerd copy of file
var lower = sourceLine.ToLower();
return this.searchList.Any(sourceLine.Contains);
});
// store current file matches
foreach (var currentMatch in currentFileMatches)
{
results.Add(currentMatch);
}
}
return results.ToArray();
}
private void ReadFiles()
{
var files = Directory.EnumerateFiles(this.searchFolder);
try
{
foreach (var file in files)
{
var fileContent = File.ReadLines(file);
// add file, or wait if filesInMemory are full
this.filesInMemory.Add(fileContent.ToArray());
}
}
finally
{
this.filesInMemory.CompleteAdding();
}
}
public void Dispose()
{
if (filesInMemory != null)
filesInMemory.Dispose();
}
}
我有一个文件夹,里面有很多 CSV 文件,每个大约 3MB。
一个 CSV 的内容示例:
afkla890sdfa9f8sadfkljsdfjas98sdf098,-1dskjdl4kjff;
afkla890sdfa9f8sadfkljsdfjas98sdf099,-1kskjd11kjsj;
afkla890sdfa9f8sadfkljsdfjas98sdf100,-1asfjdl1kjgf;
etc...
现在我有一个用 C# 编写的控制台应用程序,它在每个 CSV 文件中搜索特定字符串。 那些要搜索的字符串在一个 txt 文件中。
搜索 txt 文件的示例:
-1gnmjdl5dghs
-17kn3mskjfj4
-1plo3nds3ddd
然后我调用方法在给定文件夹的所有文件中搜索每个搜索字符串:
private static object _lockObject = new object();
public static IEnumerable<string> SearchContentListInFiles(string searchFolder, List<string> searchList)
{
var result = new List<string>();
var files = Directory.EnumerateFiles(searchFolder);
Parallel.ForEach(files, (file) =>
{
var fileContent = File.ReadLines(file);
if (fileContent.Any(x => searchList.Any(y => x.ToLower().Contains(y))))
{
lock (_lockObject)
{
foreach (string searchFound in fileContent.Where(x => searchList.Any(y => x.ToLower().Contains(y))))
{
result.Add(searchFound);
}
}
}
});
return result;
}
现在的问题是,我能否以任何方式提高此操作的性能? 我有大约 100GB 的文件要搜索。 在 SSD 磁盘和良好的 i7 CPU.
上,使用大约 25 个搜索字符串搜索所有 ~30.000 个文件大约需要 1 小时拥有更大的 CSV 文件或拥有更小的 CSV 文件会有不同吗?我只是希望这个搜索尽可能快。
更新
我已经尝试了你写的每一个建议,现在这对我来说是最好的(从 LINQ 中删除 ToLower 产生了最好的性能提升。从 1 小时开始的搜索时间现在是 16 分钟!):
public static IEnumerable<string> SearchContentListInFiles(string searchFolder, HashSet<string> searchList)
{
var result = new BlockingCollection<string>();
var files = Directory.EnumerateFiles(searchFolder);
Parallel.ForEach(files, (file) =>
{
var fileContent = File.ReadLines(file); //.Select(x => x.ToLower());
if (fileContent.Any(x => searchList.Any(y => x.Contains(y))))
{
foreach (string searchFound in fileContent.Where(x => searchList.Any(y => x.Contains(y))))
{
result.Add(searchFound);
}
}
});
return result;
}
此操作首先是磁盘绑定。磁盘绑定操作不会从多线程中受益。事实上,你要做的就是同时用大量冲突请求淹没磁盘控制器,像 NCQ 这样的功能必须再次被删除。
如果您先将所有文件加载到内存中,您的操作将是内存绑定。并且内存绑定操作也不会从多线程中受益(通常;它会在此处详细介绍 CPU 和内存架构)。
虽然一定数量的多任务在编程中是强制性的,但真正的多线程仅有助于CPU 绑定操作。那里看起来没有任何内容 CPU 绑定。所以多线程taht搜索(每个文件一个线程)不会让它更快。由于所有线程切换和同步开销,确实可能会使它变慢。
可能 Lucene 之类的东西可以提高性能:为什么不索引数据以便轻松搜索?
您将避免按顺序搜索数据。此外,您可以基于相同的数据对多个索引进行建模,以便能够以光速获得某些结果。
尝试:
对一行执行 .ToLower 一次,而不是对 searchList 中的每个元素执行 .ToLower。
对文件进行一次扫描,而不是通过任意位置扫描两次。获取列表,如果找到则添加锁。在您的示例中,您在搜索和添加时浪费了两次传递的时间并阻塞了所有线程。
如果您知道要查找的位置(在您的示例中您知道),您可以从位置扫描,而不是在所有字符串中扫描
使用生产者消费者模式例如使用:
BlockingCollection<T>
,所以不需要使用锁如果需要在字段中严格搜索,构建searchList的HashSet并执行searchHash.Contains(fieldValue)这将显着增加进程
所以这里有一个示例(未测试):
using(var searcher = new FilesSearcher(
searchFolder: "path",
searchList: toLookFor))
{
searcher.SearchContentListInFiles();
}
这里是搜索者:
public class FilesSearcher : IDisposable
{
private readonly BlockingCollection<string[]> filesInMemory;
private readonly string searchFolder;
private readonly string[] searchList;
public FilesSearcher(string searchFolder, string[] searchList)
{
// reader thread stores lines here
this.filesInMemory = new BlockingCollection<string[]>(
// limit count of files stored in memory, so if processing threads not so fast, reader will take a break and wait
boundedCapacity: 100);
this.searchFolder = searchFolder;
this.searchList = searchList;
}
public IEnumerable<string> SearchContentListInFiles()
{
// start read,
// we not need many threads here, probably 1 thread by 1 storage device is the optimum
var filesReaderTask = Task.Factory.StartNew(ReadFiles, TaskCreationOptions.LongRunning);
// at least one proccessing thread, because reader thread is IO bound
var taskCount = Math.Max(1, Environment.ProcessorCount - 1);
// start search threads
var tasks = Enumerable
.Range(0, taskCount)
.Select(x => Task<string[]>.Factory.StartNew(Search, TaskCreationOptions.LongRunning))
.ToArray();
// await for results
Task.WaitAll(tasks);
// combine results
return tasks
.SelectMany(t => t.Result)
.ToArray();
}
private string[] Search()
{
// if you always get unique results use list
var results = new List<string>();
//var results = new HashSet<string>();
foreach (var content in this.filesInMemory.GetConsumingEnumerable())
{
// one pass by a file
var currentFileMatches = content
.Where(sourceLine =>
{
// to lower one time for a line, and we don't need to make lowerd copy of file
var lower = sourceLine.ToLower();
return this.searchList.Any(sourceLine.Contains);
});
// store current file matches
foreach (var currentMatch in currentFileMatches)
{
results.Add(currentMatch);
}
}
return results.ToArray();
}
private void ReadFiles()
{
var files = Directory.EnumerateFiles(this.searchFolder);
try
{
foreach (var file in files)
{
var fileContent = File.ReadLines(file);
// add file, or wait if filesInMemory are full
this.filesInMemory.Add(fileContent.ToArray());
}
}
finally
{
this.filesInMemory.CompleteAdding();
}
}
public void Dispose()
{
if (filesInMemory != null)
filesInMemory.Dispose();
}
}