如何用C#的TPL/GPU加速百万行0/1序列频率统计程序
优化100万行0/1序列频率统计的方案(从40分钟到秒级)
你的核心问题是原代码采用了O(n²)的暴力匹配逻辑——每个序列都要遍历整个数组查找匹配,100万左右的序列量直接导致了约1万亿次对比,这才是耗时的根源。先从算法层面优化到O(n),再结合并行或GPU加速,完全能把时间压缩到秒级。
第一步:算法层面的根本优化(CPU单线程就能秒级完成)
先抛弃暴力匹配,用哈希表(Dictionary)一次遍历统计所有序列的出现次数,这是最有效的优化:
优化后的单线程代码
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; public class Program { static void Main(string[] args) { Stopwatch time = Stopwatch.StartNew(); try { string fileName = "10.txt"; int sequenceLength = 15; // 直接读取文件内容并移除换行符(比ReadAllLines+Join高效得多) string data = File.ReadAllText(fileName).Replace(Environment.NewLine, "").Replace("\n", "").Replace("\r", ""); int totalSequences = data.Length - sequenceLength + 1; // 用Dictionary统计频率,O(n)时间复杂度 Dictionary<string, int> frequencyMap = new Dictionary<string, int>(totalSequences); for (int i = 0; i < totalSequences; i++) { string sequence = data.Substring(i, sequenceLength); if (frequencyMap.ContainsKey(sequence)) { frequencyMap[sequence]++; } else { frequencyMap[sequence] = 1; } } // 写入结果(这里可以按原需求输出每个序列的次数,或者优化为只输出唯一序列+次数) using (StreamWriter sw = new StreamWriter(Path.ChangeExtension(fileName, "Results.txt"))) { // 如果需要和原代码一样,遍历所有序列输出对应次数: for (int i = 0; i < totalSequences; i++) { string sequence = data.Substring(i, sequenceLength); sw.WriteLine(frequencyMap[sequence]); } // 或者更高效的:只输出唯一序列及其次数 // foreach (var kvp in frequencyMap) { // sw.WriteLine($"{kvp.Key}: {kvp.Value}"); // } } time.Stop(); Console.WriteLine($"Time: {time.Elapsed}"); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine($"Exception: {ex.StackTrace}"); Console.ReadLine(); } } }
这个单线程版本就能把时间从40分钟压缩到几秒内,因为哈希表的查找和插入都是O(1)平均时间复杂度,总操作次数只有约100万次,而非1万亿次。
第二步:用TPL/Parallel.For进一步加速(多线程优化)
如果想利用你的6核i7-5820K的多线程能力,可以用Parallel.For结合线程安全的ConcurrentDictionary,或者分区处理后合并结果:
并行版代码
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; public class Program { static void Main(string[] args) { Stopwatch time = Stopwatch.StartNew(); try { string fileName = "10.txt"; int sequenceLength = 15; string data = File.ReadAllText(fileName).Replace(Environment.NewLine, "").Replace("\n", "").Replace("\r", ""); int totalSequences = data.Length - sequenceLength + 1; // 线程安全的ConcurrentDictionary用于并行统计 ConcurrentDictionary<string, int> frequencyMap = new ConcurrentDictionary<string, int>(); // 用Parallel.For并行遍历,利用多核心 Parallel.For(0, totalSequences, i => { string sequence = data.Substring(i, sequenceLength); frequencyMap.AddOrUpdate(sequence, 1, (key, count) => count + 1); }); // 写入结果 using (StreamWriter sw = new StreamWriter(Path.ChangeExtension(fileName, "Results.txt"))) { for (int i = 0; i < totalSequences; i++) { string sequence = data.Substring(i, sequenceLength); sw.WriteLine(frequencyMap[sequence]); } } time.Stop(); Console.WriteLine($"Time: {time.Elapsed}"); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine($"Exception: {ex.StackTrace}"); Console.ReadLine(); } } }
这个版本能把时间再压缩一点,尤其是当数据量更大的时候,你的6核CPU能充分发挥作用。
第三步:GPU加速方案(极致性能,适合超大规模数据)
如果你的数据量继续增长,或者追求极致的速度,可以利用你的双GTX 970显卡做GPU加速。核心思路是把字符串序列转换为整数(避免字符串操作的开销),然后用GPU的并行计算能力做直方图统计:
实现步骤:
- 序列转整数:15位二进制序列可以直接转换为一个
ushort(无符号16位整数,范围0~32767),完全不会丢失前导零(比如"000000000000001"对应整数1,唯一对应)。 - GPU直方图统计:GPU擅长并行处理大量相同的计算,我们可以把所有转换后的整数传到GPU,用CUDA核函数并行统计每个整数的出现次数。
- 结果回传:把GPU统计的结果传回CPU,再映射回对应的二进制序列输出。
示例代码(用ManagedCUDA库)
首先需要安装ManagedCUDA NuGet包,然后实现:
using System; using System.Diagnostics; using System.IO; using ManagedCuda; using ManagedCuda.VectorTypes; public class GpuSequenceCounter { static void Main(string[] args) { Stopwatch time = Stopwatch.StartNew(); string fileName = "10.txt"; int sequenceLength = 15; // 读取并处理数据 string data = File.ReadAllText(fileName).Replace(Environment.NewLine, "").Replace("\n", "").Replace("\r", ""); int totalSequences = data.Length - sequenceLength + 1; // 把所有序列转换为ushort数组 ushort[] sequenceInts = new ushort[totalSequences]; for (int i = 0; i < totalSequences; i++) { string seq = data.Substring(i, sequenceLength); sequenceInts[i] = Convert.ToUInt16(seq, 2); } // 初始化CUDA CudaContext ctx = new CudaContext(0); // 使用第一块GTX970 int histogramSize = 1 << sequenceLength; // 2^15=32768 // 分配GPU内存 CudaDeviceVariable<ushort> d_sequenceInts = sequenceInts; CudaDeviceVariable<int> d_histogram = new int[histogramSize]; d_histogram.SetZero(); // 加载CUDA核函数(这里需要提前写好.cu文件编译为.ptx) // 核函数逻辑:每个线程处理一个整数,原子递增对应直方图位置 string ptxPath = @"HistogramKernel.ptx"; CudaKernel kernel = ctx.LoadKernelPTX(ptxPath, "CountSequences"); // 启动核函数:每个线程处理一个元素,用足够的线程块 int threadsPerBlock = 256; int blocksPerGrid = (totalSequences + threadsPerBlock - 1) / threadsPerBlock; kernel.BlockDimensions = new dim3(threadsPerBlock, 1, 1); kernel.GridDimensions = new dim3(blocksPerGrid, 1, 1); kernel.Run(d_sequenceInts.DevicePointer, d_histogram.DevicePointer, totalSequences); // 把直方图结果传回CPU int[] histogram = d_histogram; // 写入结果 using (StreamWriter sw = new StreamWriter(Path.ChangeExtension(fileName, "Results.txt"))) { for (int i = 0; i < totalSequences; i++) { ushort seqInt = sequenceInts[i]; sw.WriteLine(histogram[seqInt]); } } // 释放资源 d_sequenceInts.Dispose(); d_histogram.Dispose(); ctx.Dispose(); time.Stop(); Console.WriteLine($"Time: {time.Elapsed}"); Console.ReadLine(); } }
对应的CUDA核函数(HistogramKernel.cu):
extern "C" __global__ void CountSequences(ushort* sequences, int* histogram, int total) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx < total) { ushort seq = sequences[idx]; atomicAdd(&histogram[seq], 1); } }
这个GPU版本能把时间压缩到毫秒级,尤其适合超大规模数据,你的双GTX970还可以做多GPU并行进一步加速。
额外小优化
- 读取文件时,用
File.ReadAllText替代File.ReadAllLines+string.Join,减少字符串拼接的开销。 - 如果不需要输出每个序列的次数,只输出唯一序列及其频率,可以直接遍历哈希表或GPU直方图,避免二次遍历原序列,进一步节省时间。
内容的提问来源于stack exchange,提问作者Zain Arshad
相关产品推荐
相关产品推荐

