从一个长字符串(300万个字符)中读取大量(100万个字符宽)的子字符串

本文关键字:字符 字符串 100万 300万 一个 读取 | 更新日期: 2023-09-27 18:28:33

如何在C#中有效地从一个超过300万个字符的字符串中提取100万个子字符串?我写了一个程序,它涉及从一个300万个字符的字符串中读取长度为100的随机DNA读数(来自随机位置的子字符串)。这样的阅读量有100万。目前,我运行了一个while循环,运行了100万次,并从300万个字符的字符串中读取了一个100个字符长的子字符串。这需要很长时间。我该怎么做才能更快地完成这项工作?

在我的代码中,len是原始字符串的长度,在这种情况下为300万,可能低至50,这就是while循环中检查的原因。

while(i < 1000000 && len-100> 0) //len is 3000000
            {
                int randomPos = _random.Next()%(len - ReadLength);
                readString += all.Substring(randomPos, ReadLength) + Environment.NewLine;
                i++;

            }

从一个长字符串(300万个字符)中读取大量(100万个字符宽)的子字符串

使用StringBuilder组装字符串将使处理量增加600倍(因为它避免了每次附加到字符串时重复创建对象。

循环之前(初始化容量可以避免在StringBuilder中重新创建后备数组):

StringBuilder sb = new StringBuilder(1000000 * ReadLength);

环路中:

sb.Append(all.Substring(randomPos, ReadLength) + Environment.NewLine);

后循环:

readString = sb.ToString();

使用char数组而不是字符串来提取值可以再提高30%,因为在调用Substring()时可以避免创建对象:

循环前:

char[] chars = all.ToCharArray();

环路中:

sb.Append(chars, randomPos, ReadLength);
sb.AppendLine();

编辑(不使用StringBuilder并在300ms内执行的最终版本):

char[] chars = all.ToCharArray();    
var iterations = 1000000;
char[] results = new char[iterations * (ReadLength + 1)];    
GetRandomStrings(len, iterations, ReadLength, chars, results, 0);    
string s = new string(results);
private static void GetRandomStrings(int len, int iterations, int ReadLength, char[] chars, char[] result, int resultIndex)
{
    Random random = new Random();
    int i = 0, index = resultIndex;
    while (i < iterations && len - 100 > 0) //len is 3000000 
    {
        var i1 = len - ReadLength;
        int randomPos = random.Next() % i1;
        Array.Copy(chars, randomPos, result, index, ReadLength);
        index += ReadLength;
        result[index] = Environment.NewLine[0];
        index++;
        i++;
    }
}

我认为会有更好的解决方案,但.NET StringBuilder类实例比String类实例更快,因为它将数据作为流处理。

您可以将数据拆分为多个部分,并使用.NET任务并行库进行多线程和并行

编辑:为循环外的变量指定固定值,以避免重新计算;

int x = len-100 
int y = len-ReadLength 

使用

StringBuilder readString= new StringBuilder(ReadLength * numberOfSubStrings);
readString.AppendLine(all.Substring(randomPos, ReadLength));

对于并行性,您应该将输入拆分为多个部分。然后在单独的线程中对工件运行这些操作。然后将结果合并。

重要信息:正如我之前的经验所表明的,这些操作在.NET v2.0而不是v4.0中运行得更快,因此您应该更改项目的目标框架版本;但是你不能在.NET v2.0中使用任务并行库,所以你应该像一样使用老式的多线程

Thread newThread ......

长时间有多长?应该不会那么久。

var file = new StreamReader(@"E:'Temp'temp.txt");
var s = file.ReadToEnd();
var r = new Random();
var sw = new Stopwatch();
sw.Start();
var range = Enumerable.Range(0,1000000);
var results = range.Select( i => s.Substring(r.Next(s.Length - 100),100)).ToList();
sw.Stop();
sw.ElapsedMilliseconds.Dump();
s.Length.Dump();

所以在我的机器上,结果是807ms,字符串是4055442个字符。

编辑:我刚刚注意到你想要一个字符串作为结果,所以我上面的解决方案只是改为…

var results = string.Join(Environment.NewLine,range.Select( i => s.Substring(r.Next(s.Length - 100),100)).ToArray());

并且增加了大约100毫秒,所以总共还不到一秒。

Edit:我放弃了使用memcpy的想法,我认为结果非常棒。我在43毫秒内把一根3米长的绳子分成了30公里长的100根绳子。

private static unsafe string[] Scan(string hugeString, int subStringSize)
{
    var results = new string[hugeString.Length / subStringSize];
    var gcHandle = GCHandle.Alloc(hugeString, GCHandleType.Pinned);
    var currAddress = (char*)gcHandle.AddrOfPinnedObject();
    for (var i = 0; i < results.Length; i++)
    {
        results[i] = new string(currAddress, 0, subStringSize);
        currAddress += subStringSize;
    }
    return results;
}

使用问题中所示的方法:

const int size = 3000000;
const int subSize = 100;
var stringBuilder = new StringBuilder(size);
var random = new Random();
for (var i = 0; i < size; i++)
{
    stringBuilder.Append((char)random.Next(30, 80));
}
var hugeString = stringBuilder.ToString();
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
    var strings = Scan(hugeString, subSize);
}
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds / 1000); // 43