探讨如何加快 C# 双循环的速度效率
- 一、前言
- 二、多种循环的速度效率
- 三、如何提高双循环计算速度效率的编程实例
-
- 3.1、中规中矩的双循环
- 3.2、采用改进二分法的双循环
- [3.3 、采用并行循环](#3.3 、采用并行循环)
- 四、双循环更好的办法
一、前言
最近使用 C# 改写以前编的一个程序,检索数据赋值时,使用 FOR 循环结构,当数据量在9万条时,计算量很大,导致很耗时,何况计算完成同时加载到 chart 和 dataGridView 图表控件中(没有使用第三方控件),于是百度了很多循环手段。
二、多种循环的速度效率
2.1、常用循环速度效率对比
循环结构体 | 速度和效率 |
---|---|
for(int i=0;i<n;i++){} | 对于已知次数的迭代,使用 for 循环通常是最快的。 |
foreach(string S in Data){} | 适用于遍历集合,如数组或列表,但通常比 for 循环慢,因为它需要更多的间接访问。 |
while do-while | 适用于不确定次数的迭代,但可能在性能上不如 for 循环。 |
Parallel.For Parallel.ForEach Parallel.Invoke | 并行计算类,三个方法都会阻塞线程直到所有工作完成为止。仅当适用时可采用,否则出现意外错误。 Parallel.ForEach 比 Parallel.For 效率更高。 Parallel.Invoke 适合用于执行大量且无返回值的场景; Parallel.For 使用是无序的,适合带索引的大量循环操作; Parallel.ForEach 适合数组、集合、枚举大数据集的循环执行,执行结果是无序的。 |
2.2、如何提高循环的速度效率
2.2.1、选择合适的循环类型
循环的效率通常取决于多个因素,包括循环的类型、循环体内的操作、以及循环的次数。选择合适的循环类型很重要。
2.2.2、减少循环体内的操作
循环体内的操作越少,循环的效率越高。例如,避免在每次迭代中进行复杂的计算或方法调用。
2.2.3、使用局部变量而非频繁访问成员
如果循环体内需要多次访问对象的成员,最好将该成员的值存储在局部变量中,这样可以减少多次访问成员的开销。
2.2.4、避免在循环体内进行不必要的对象创建或销毁
每次迭代创建新对象会增加GC(垃圾回收)的压力,降低性能。如果可能,考虑对象池技术或复用对象。
2.2.5、仅当适用时可采用并行处理
对于可以并行处理的计算任务,使用 Parallel.For 或 PLINQ(Parallel LINQ)可以显著提高性能,特别是当处理大量数据时。然而,并行处理需要谨慎使用,因为它会增加线程管理的开销。
三、如何提高双循环计算速度效率的编程实例
遵循上述循环原则,对于有序的数据集,通过多次测试各种循环,采用的 for 双循环。
Hypack RAW 数据中,实时采集的定位 POS 数据和 EC1 数据系列,时间和位置大部分不同步,但处于同一个航线轨迹上,通过各采集点的时间秒、坐标、水深,计算航行距离、航行速度,以计算插补 EC1 采集数据所在定位坐标。
POS 数据有时间、定位东和北坐标数组,POSTime[]、POSEast[]、POSNorth[] 数据长度一致。
EC1 数据有时间、水深数组,EC1Time[]、EC1WaterDepth[] 数据长度一致。
POS 和 EC1 数据个数不对等。
还要根据 FIX 和 EC1 数据,插补 FIX 采集数据点的水深,这里就不描述。
Hypack RAW 数据节选如下:
bash
POS 0 37410.599 626291.650 3216635.552
QUA 0 37410.599 7 3.000 1.000 8.000 2.000 0.000 0.000 0.000
RAW 0 37410.599 4 290361.17840 1121780.82570 9.91600 22330.60000
MSG 0 37410.538 $GPGGA,022330.60,2903.611784,N,11217.808257,E,2,08,1.0,9.916,M,0.0,M,8.0,0643*71
MSG 0 37410.581 $GPVTG,22.3,T,,M,0.08,N,0.15,K,P*23
MSG 0 37410.600 $GPZDA,022330.60,27,03,2024,00,00*62
EC1 1 37410.736 1.040
POS 0 37410.799 626291.660 3216635.561
QUA 0 37410.799 7 3.000 1.000 8.000 2.000 0.000 0.000 0.000
RAW 0 37410.799 4 290361.17890 1121780.82630 9.90800 22330.80000
MSG 0 37410.735 $GPGGA,022330.80,2903.611789,N,11217.808263,E,2,08,1.0,9.908,M,0.0,M,8.0,0643*7A
MSG 0 37410.778 $GPVTG,42.9,T,,M,0.15,N,0.27,K,P*22
MSG 0 37410.797 $GPZDA,022330.80,27,03,2024,00,00*6C
EC1 1 37410.939 1.040
POS 0 37411.000 626291.673 3216635.574
QUA 0 37411.000 7 3.000 1.000 8.000 2.000 0.000 0.000 0.000
RAW 0 37411.000 4 290361.17960 1121780.82710 9.89800 22331.00000
MSG 0 37410.938 $GPGGA,022331.00,2903.611796,N,11217.808271,E,2,08,1.0,9.898,M,0.0,M,8.0,0643*76
MSG 0 37410.981 $GPVTG,46.0,T,,M,0.17,N,0.31,K,P*2A
MSG 0 37411.001 $GPZDA,022331.00,27,03,2024,00,00*65
FIX 99 37411.109 48 626291.673 3216635.574
EC1 1 37411.138 1.030
POS 0 37411.200 626291.687 3216635.584
QUA 0 37411.200 7 3.000 1.000 8.000 2.000 0.000 0.000 0.000
RAW 0 37411.200 4 290361.18010 1121780.82800 9.90800 22331.20000
MSG 0 37411.137 $GPGGA,022331.20,2903.611801,N,11217.808280,E,2,08,1.0,9.908,M,0.0,M,4.0,0643*7F
MSG 0 37411.180 $GPVTG,54.9,T,,M,0.21,N,0.39,K,P*2D
MSG 0 37411.199 $GPZDA,022331.20,27,03,2024,00,00*67
EC1 1 37411.337 1.020
POS 0 37411.400 626291.708 3216635.595
QUA 0 37411.400 7 3.000 1.000 8.000 2.000 0.000 0.000 0.000
RAW 0 37411.400 4 290361.18070 1121780.82930 9.90400 22331.40000
MSG 0 37411.336 $GPGGA,022331.40,2903.611807,N,11217.808293,E,2,08,1.0,9.904,M,0.0,M,4.0,0643*71
MSG 0 37411.379 $GPVTG,57.9,T,,M,0.23,N,0.42,K,P*20
MSG 0 37411.398 $GPZDA,022331.40,27,03,2024,00,00*61
EC1 1 37411.538 1.030
POS 0 37411.599 626291.727 3216635.606
QUA 0 37411.599 7 3.000 1.000 8.000 2.000 0.000 0.000 0.000
RAW 0 37411.599 4 290361.18130 1121780.83050 9.90100 22331.60000
MSG 0 37411.537 $GPGGA,022331.60,2903.611813,N,11217.808305,E,2,08,1.0,9.901,M,0.0,M,4.0,0643*7D
MSG 0 37411.580 $GPVTG,61.7,T,,M,0.22,N,0.41,K,P*29
MSG 0 37411.600 $GPZDA,022331.60,27,03,2024,00,00*63
EC1 1 37411.737 1.020
POS 0 37411.799 626291.750 3216635.618
QUA 0 37411.799 7 3.000 1.000 8.000 2.000 0.000 0.000 0.000
RAW 0 37411.799 4 290361.18190 1121780.83190 9.89600 22331.80000
MSG 0 37411.736 $GPGGA,022331.80,2903.611819,N,11217.808319,E,2,08,1.0,9.896,M,0.0,M,4.0,0643*7B
MSG 0 37411.779 $GPVTG,64.7,T,,M,0.25,N,0.45,K,P*2F
MSG 0 37411.798 $GPZDA,022331.80,27,03,2024,00,00*6D
EC1 1 37411.937 1.030
POS 0 37412.000 626291.774 3216635.627
QUA 0 37412.000 7 3.000 1.000 8.000 2.000 0.000 0.000 0.000
RAW 0 37412.000 4 290361.18240 1121780.83340 9.89900 22332.00000
MSG 0 37411.937 $GPGGA,022332.00,2903.611824,N,11217.808334,E,2,08,1.0,9.899,M,0.0,M,4.0,0643*7E
MSG 0 37411.980 $GPVTG,68.6,T,,M,0.25,N,0.47,K,P*20
MSG 0 37411.999 $GPZDA,022332.00,27,03,2024,00,00*66
EC1 1 37412.138 1.020
POS 0 37412.200 626291.790 3216635.646
3.1、中规中矩的双循环
以下代码实例中,是 Hypack RAW 数据处理中的双循环,插补 EC1 采集数据所在定位坐标。
数据的时间秒从小到大有序排列。
csharp
for (int j = 0; j < POSLeng; j++)
{
if (j == 0 && EC1Time[i] < POSTime[j])//EC1时间小于第一个POS时间
{
EC1East[i] = POSEast[j] - (POSEast[j + 1] - POSEast[j]) / (POSTime[j + 1] - POSTime[j]) * (POSTime[j] - EC1Time[i]);//水深点东坐标
EC1North[i] = POSNorth[j] - (POSNorth[j + 1] - POSNorth[j]) / (POSTime[j + 1] - POSTime[j]) * (POSTime[j] - EC1Time[i]);//水深点北坐标
break;
}
if (j == POSLeng - 1 && EC1Time[i] > POSTime[POSLeng - 1])//EC1时间大于最后一个POS时间
{
EC1East[i] = POSEast[j] + (POSEast[j] - POSEast[j - 1]) / (POSTime[j] - POSTime[j - 1]) * (EC1Time[i] - POSTime[j]);//水深点东坐标
EC1North[i] = POSNorth[j] + (POSNorth[j] - POSNorth[j - 1]) / (POSTime[j] - POSTime[j - 1]) * (EC1Time[i] - POSTime[j]);//水深点北坐标
break;
}
if (EC1Time[i] == POSTime[j])
{
EC1East[i] = POSEast[j];
EC1North[i] = POSNorth[j];
break;
}
if ((EC1Time[i] > POSTime[j]) && (EC1Time[i] < POSTime[j + 1]))
{
EC1East[i] = POSEast[j] + (POSEast[j + 1] - POSEast[j]) / (POSTime[j + 1] - POSTime[j]) * (EC1Time[i] - POSTime[j]);//水深点东坐标
EC1North[i] = POSNorth[j] + (POSNorth[j + 1] - POSNorth[j]) / (POSTime[j + 1] - POSTime[j]) * (EC1Time[i] - POSTime[j]);//水深点北坐标
break;
}
}
SurveyEC1[i, 0] = EC1Time[i];
SurveyEC1[i, 1] = EC1East[i];
SurveyEC1[i, 2] = EC1North[i];
SurveyEC1[i, 3] = EC1WaterDepth[i];
以上代码循环效率不高,测试中,当 RAW 文件 3119 KB,EC1 数据量达到 97981 个,POS数据量达到 6142 个,FIX数据量达到 503个。插补 EC1 采集点的坐标和 从 EC1 采集点检索 FIX 采集的水深,很耗时,达到 2 千毫秒左右,插补数据后,还要加载到 chart 和 dataGridView 图表控件中,加载图标达到 7 百毫秒左右,导致显示很慢。
3.2、采用改进二分法的双循环
为了提高运算速度和效率,采用改进二分法的双循环,根据循环原则,优化代码结构,使用类封装坐标计算循环为 InterpCoord 函数,二分法循环调用它。插补 EC1 采集点的坐标和 FIX 采集的水深,整体循环减小到 764 毫秒左右。
csharp
//根据 POS 定位数据和时间数据,以航速计算插补 EC1 水深的定位坐标
//改进二分法,加快循环
int EC1Leng = ListEc1Time.Length;//EC1数据个数
int MedianNum = 0;
if ((EC1Leng & 1) == 0)
{
MedianNum = EC1Leng / 2;//偶数,数据半数
}
else
{
MedianNum = (int)(EC1Leng / 2) + 1; //奇数,数据半数+1
}
for (int i = 0; i <= MedianNum; i++)//for (int i = 0; i < EC1Leng; i++)
{
//从时间秒最小索引 i = 0 开始顺序计算到中数
InterpCoord(EC1Time[i], ref EC1East[i], ref EC1North[i], POSTime, POSEast, POSNorth);
SurveyEC1[i, 0] = EC1Time[i];
SurveyEC1[i, 1] = EC1East[i];
SurveyEC1[i, 2] = EC1North[i];
SurveyEC1[i, 3] = EC1WaterDepth[i];
//从时间秒最大索引 i = EC1Leng - 1 开始逆序计算到中数
int ReverseOrderNum = EC1Leng - i - 1;
InterpCoord(EC1Time[ReverseOrderNum], ref EC1East[ReverseOrderNum], ref EC1North[ReverseOrderNum], POSTime, POSEast, POSNorth);
//组合为二维 EC1 数据
SurveyEC1[ReverseOrderNum, 0] = EC1Time[ReverseOrderNum];
SurveyEC1[ReverseOrderNum, 1] = EC1East[ReverseOrderNum];
SurveyEC1[ReverseOrderNum, 2] = EC1North[ReverseOrderNum];
SurveyEC1[ReverseOrderNum, 3] = EC1WaterDepth[ReverseOrderNum];
}
使用类封装坐标计算循环为 InterpCoord 函数,以便构造引用:
csharp
public class Hypack
{
/// <summary>计算 EC1 时间点的坐标</summary>
/// <param name="InterpTime">EC1时间点</param>
/// <param name="InterpEast">EC1东坐标</param>
/// <param name="InterpNorth">EC1西坐标</param>
/// <param name="LineTime">POS时间点</param>
/// <param name="LineEast">POS东坐标</param>
/// <param name="LineNorth">POS西坐标</param>
public static void InterpCoord(double InterpTime, ref double InterpEast, ref double InterpNorth, double[] LineTime, double[] LineEast, double[] LineNorth)
{
int LineLeng = LineTime.Length;
if (InterpTime < LineTime[0])//EC1时间小于第一个 POS 线上时间,计算EC1时间对应坐标
{
InterpEast = LineEast[0] - (LineEast[1] - LineEast[0]) / (LineTime[1] - LineTime[0]) * (LineTime[0] - InterpTime);//水深点东坐标
InterpNorth = LineNorth[0] - (LineNorth[1] - LineNorth[0]) / (LineTime[1] - LineTime[0]) * (LineTime[0] - InterpTime);//水深点北坐标
}
else if (InterpTime > LineTime[LineLeng - 1])//EC1时间大于最后一个 POS 线上时间,计算EC1时间对应坐标
{
InterpEast = LineEast[LineLeng - 1] + (LineEast[LineLeng - 1] - LineEast[LineLeng - 2]) / (LineTime[LineLeng - 1] - LineTime[LineLeng - 2]) * (InterpTime - LineTime[LineLeng - 1]);//水深点东坐标
InterpNorth = LineNorth[LineLeng - 1] + (LineNorth[LineLeng - 1] - LineNorth[LineLeng - 2]) / (LineTime[LineLeng - 1] - LineTime[LineLeng - 2]) * (InterpTime - LineTime[LineLeng - 1]);//水深点北坐标
}
else
{
for (int j = 0; j < LineLeng; j++)
{
if (InterpTime == LineTime[j])//EC1时间等于 POS 线上时间,坐标一致
{
InterpEast = LineEast[j];
InterpNorth = LineNorth[j];
break;
}
if ((InterpTime > LineTime[j]) && (InterpTime < LineTime[j + 1]))//EC1时间在 POS 线上 2 个时间之间,计算EC1时间对应坐标
{
InterpEast = LineEast[j] + (LineEast[j + 1] - LineEast[j]) / (LineTime[j + 1] - LineTime[j]) * (InterpTime - LineTime[j]);//水深点东坐标
InterpNorth = LineNorth[j] + (LineNorth[j + 1] - LineNorth[j]) / (LineTime[j + 1] - LineTime[j]) * (InterpTime - LineTime[j]);//水深点北坐标
break;
}
}
}
// 尝试如下代码更耗时,相当于增加了循环,尽管代码简洁
找到大于目标的第一个元素的索引
//int indexGreaterThan = Array.FindIndex(LineTime, n => n > InterpTime);
找到等于于target的第一个元素的索引
//int indexEqualThan = Array.FindIndex(LineTime, n => n == InterpTime);
找到小于target的第一个元素的索引
//int indexLessThan = Array.FindLastIndex(LineTime, n => n < InterpTime);
//if (indexEqualThan >= 0)
//{
// InterpEast = LineEast[indexEqualThan];
// InterpNorth = LineNorth[indexEqualThan];
//}
//if (indexLessThan >= 0 && indexGreaterThan >= 0)
//{
// InterpEast = LineEast[indexLessThan] + (LineEast[indexGreaterThan] - LineEast[indexLessThan]) / (LineTime[indexGreaterThan] - LineTime[indexLessThan]) * (InterpTime - LineTime[indexLessThan]);//水深点东坐标
// InterpNorth = LineNorth[indexLessThan] + (LineNorth[indexGreaterThan] - LineNorth[indexLessThan]) / (LineTime[indexGreaterThan] - LineTime[indexLessThan]) * (InterpTime - LineTime[indexLessThan]);//水深点北坐标
//}
}
}
3.3 、采用并行循环
为了提高到更快的运算速度和效率,采用并行循环,优化代码结构,调用类封装的坐标计算循环为 InterpCoord 函数。插补 EC1 采集点的坐标和 FIX 采集的水深,整体循环减小到 377 毫秒左右。
csharp
Parallel.For(0, EC1Leng, j =>
{
InterpCoord(EC1Time[j], ref EC1East[j], ref EC1North[j], POSTime, POSEast, POSNorth);
SurveyEC1[j, 0] = EC1Time[j];
SurveyEC1[j, 1] = EC1East[j];
SurveyEC1[j, 2] = EC1North[j];
SurveyEC1[j, 3] = EC1WaterDepth[j];
});
四、双循环更好的办法
多线程处理不当容易内存溢出,线程间的错误处理和同步问题。需要严谨的避免多个线程同时访问同一资源,否则导致冲突或错误。存在内存销毁消耗,处理不当反而浪费时间,增加开销。其它方法还在探索中...
你有更好的建议吗?