You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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的并行计算能力做直方图统计:

实现步骤:

  1. 序列转整数:15位二进制序列可以直接转换为一个ushort(无符号16位整数,范围0~32767),完全不会丢失前导零(比如"000000000000001"对应整数1,唯一对应)。
  2. GPU直方图统计:GPU擅长并行处理大量相同的计算,我们可以把所有转换后的整数传到GPU,用CUDA核函数并行统计每个整数的出现次数。
  3. 结果回传:把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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:21:36