探讨如何加快 C# 双循环的速度效率

探讨如何加快 C# 双循环的速度效率

一、前言

最近使用 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];
   });

四、双循环更好的办法

多线程处理不当容易内存溢出,线程间的错误处理和同步问题。需要严谨的避免多个线程同时访问同一资源,否则导致冲突或错误。‌存在内存销毁消耗,处理不当反而浪费时间,增加开销。其它方法还在探索中...

你有更好的建议吗?

相关推荐
视觉人机器视觉1 小时前
机器视觉中的3D高反光工件检测
人工智能·3d·c#·视觉检测
CodeCraft Studio1 小时前
文档处理控件TX Text Control系列教程:使用 .NET C# 从 PDF 文档中提取基于模板的文本
pdf·c#·.net
一念春风2 小时前
C# 背景 透明 抗锯齿 (效果完美)
开发语言·c#
且听风吟ayan2 小时前
leetcode day19 844+977
leetcode·c#
C137的本贾尼2 小时前
解决 LeetCode 串联所有单词的子串问题
算法·leetcode·c#
CoderIsArt5 小时前
C# 中的一个特性(Attribute)[ThreadStatic]
c#
心疼你的一切5 小时前
C# 中关于补位的写法 PadLeft,PadRight 函数
开发语言·unity·c#·游戏引擎·csdn·心疼你的一切
yue0085 小时前
C#项目05-猜数字多线程
c#·多线程·猜数字·invoke·control·项目案例·methodinvoke
视觉人机器视觉6 小时前
机器视觉3D深度图颜色含义解析
人工智能·3d·c#·视觉检测
yujunl6 小时前
用大内存主机下载Visual Studio
c#