为啥在 C# 中的二维数组中按列写入速度很慢
Posted
技术标签:
【中文标题】为啥在 C# 中的二维数组中按列写入速度很慢【英文标题】:Why writing by column is slow in two dimensional array in C#为什么在 C# 中的二维数组中按列写入速度很慢 【发布时间】:2021-04-06 19:32:01 【问题描述】:当我按列添加值时,我有一个二维数组,它的写入速度非常慢(小于 300 倍):
class Program
static void Main(string[] args)
TwoDimArrayPerfomrance.GetByColumns();
TwoDimArrayPerfomrance.GetByRows();
class TwoDimArrayPerfomrance
public static void GetByRows()
int maxLength = 20000;
int[,] a = new int[maxLength, maxLength];
DateTime dt = DateTime.Now;
Console.WriteLine("The current time is: " + dt.ToString());
//fill value
for (int i = 0; i < maxLength; i++)
for (int j = 0; j < maxLength; j++)
a[i, j] = i + j;
DateTime end = DateTime.Now;
Console.WriteLine("Total: " + end.Subtract(dt).TotalSeconds);
public static void GetByColumns()
int maxLength = 20000;
int[,] a = new int[maxLength, maxLength];
DateTime dt = DateTime.Now;
Console.WriteLine("The current time is: " + dt.ToString());
for (int i = 0; i < maxLength; i++)
for (int j = 0; j < maxLength; j++)
a[j, i] = j + i;
DateTime end = DateTime.Now;
Console.WriteLine("Total: " + end.Subtract(dt).TotalSeconds);
立柱虎钳大约需要 4.2 秒 而按行取 1.53
【问题讨论】:
缓存接近度可能...您是否在没有调试器的情况下在发布模式下运行程序? (Visual Studio 中的 CTRL+F5)?尝试做TwoDimArrayPerfomrance.GetByColumns(); TwoDimArrayPerfomrance.GetByRows(); TwoDimArrayPerfomrance.GetByColumns(); TwoDimArrayPerfomrance.GetByRows();
看看是否是热身问题?什么版本的 .NET Core/.NET Framework 和 32 位或 64 位?微基准测试是一门艺术,一门复杂的艺术。
啊,bidi 阵列就像 90 年代的风格......不再流行。建议使用交错数组(int[][]
)
请使用StopWatch
而不是DateTime
来衡量花费的时间。
@DmitryBychenko 谢谢我现在使用秒表,但仍然在同一时间使用
@xanatos 当我第二次调用它时,第二次迭代需要 3 秒而不是 4.
【参考方案1】:
这是第一条评论中提到的“缓存邻近”问题。有内存缓存,任何数据都必须经过 CPU 才能访问。这些缓存存储内存块,因此如果您首先访问内存 N 然后访问内存 N+1,则缓存不会更改。但是,如果您首先访问内存 N,然后访问内存 N+M(其中 M 足够大),则必须将新的内存块添加到缓存中。当您将新块添加到缓存时,必须删除一些现有块。如果您随后必须访问此已删除的块,那么您的代码效率低下。
【讨论】:
【参考方案2】:我完全同意@Dialectus 所写的...我只想补充一点,编写微基准测试有一些不好的方法,还有更糟糕的方法。进行微基准测试时有很多事情要做。记住在没有附加调试器的情况下以Release模式运行,记住有一个GC,最好在你想要它运行的时候运行,而不是在你进行基准测试时随便运行,记住有时代码只有在它至少执行一次,因此至少进行一轮全面预热是个好主意……等等……甚至还有一个完整的基准测试库 (https://benchmarkdotnet.org/articles/overview.html),供 Microscot .NET Core 团队使用检查他们编写的代码是否没有速度回归。
class Program
static void Main(string[] args)
if (Debugger.IsAttached)
Console.WriteLine("Warning, debugger attached!");
#if DEBUG
Console.WriteLine("Warning, Debug version!");
#endif
Console.WriteLine($"Running at (Environment.Is64BitProcess ? 64 : 32)bits");
Console.WriteLine(RuntimeInformation.FrameworkDescription);
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
Console.WriteLine();
const int MaxLength = 10000;
for (int i = 0; i < 10; i++)
Console.WriteLine($"Round i + 1:");
TwoDimArrayPerfomrance.GetByRows(MaxLength);
GC.Collect();
GC.WaitForPendingFinalizers();
TwoDimArrayPerfomrance.GetByColumns(MaxLength);
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine();
class TwoDimArrayPerfomrance
public static void GetByRows(int maxLength)
int[,] a = new int[maxLength, maxLength];
Stopwatch sw = Stopwatch.StartNew();
//fill value
for (int i = 0; i < maxLength; i++)
for (int j = 0; j < maxLength; j++)
a[i, j] = i + j;
sw.Stop();
Console.WriteLine($"By Rows, size maxLength * maxLength, sw.ElapsedMilliseconds / 1000.0:0.00 seconds");
// So that the assignment isn't optimized out, we do some fake operation on the array
for (int i = 0; i < maxLength; i++)
for (int j = 0; j < maxLength; j++)
if (a[i, j] == int.MaxValue)
throw new Exception();
public static void GetByColumns(int maxLength)
int[,] a = new int[maxLength, maxLength];
Stopwatch sw = Stopwatch.StartNew();
//fill value
for (int i = 0; i < maxLength; i++)
for (int j = 0; j < maxLength; j++)
a[j, i] = i + j;
sw.Stop();
Console.WriteLine($"By Columns, size maxLength * maxLength, sw.ElapsedMilliseconds / 1000.0:0.00 seconds");
// So that the assignment isn't optimized out, we do some fake operation on the array
for (int i = 0; i < maxLength; i++)
for (int j = 0; j < maxLength; j++)
if (a[i, j] == int.MaxValue)
throw new Exception();
啊... .NET 3.5 的 FooType[,]
went the way of the dodo 类型的多维数组,当 LINQ 出现但它不支持它们时。你应该使用锯齿状数组FooType[][]
。
【讨论】:
【参考方案3】:如果您尝试将二维数组映射到一维数组,可能会更容易看到发生了什么。
映射给出
var a = int[maxLength * maxLength];
现在查找计算由您决定。
for (int i = 0; i < maxLength; i++)
for (int j = 0; j < maxLength; j++)
//var rowBased = j + i * MaxLength;
var colBased = i + j * MaxLength;
//a[rowBased] = i + j;
a[colBased] = i + j;
所以请注意以下事项
在基于列的查找中,乘法的次数是 20.000 * 20.000 次,因为 j 每个循环都会发生变化 在基于行的查找中,i * MaxLength 经过编译器优化,仅发生 20.000 次。 既然 a 是一个一维数组,那么还可以更轻松地查看内存是如何被访问的。在基于行的索引上,内存是按顺序访问的,而基于列的访问几乎是随机的,并且根据数组的大小,开销会有所不同。看看 BenchmarkDotNet 产生了什么
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET Core SDK=5.0.101
Method | MaxLength | Mean | Error | StdDev |
---|---|---|---|---|
GetByRows | 100 | 23.60 us | 0.081 us | 0.076 us |
GetByColumns | 100 | 23.74 us | 0.357 us | 0.334 us |
GetByRows | 1000 | 2,333.20 us | 13.150 us | 12.301 us |
GetByColumns | 1000 | 2,784.43 us | 10.027 us | 8.889 us |
GetByRows | 10000 | 238,599.37 us | 1,592.838 us | 1,412.009 us |
GetByColumns | 10000 | 516,771.56 us | 4,272.849 us | 3,787.770 us |
GetByRows | 50000 | 5,903,087.26 us | 13,822.525 us | 12,253.308 us |
GetByColumns | 50000 | 19,623,369.45 us | 92,325.407 us | 86,361.243 us |
您会看到,虽然 MaxLength 相当小,但差异几乎可以忽略不计 (100x100) 和 (1000x1000),因为我希望 CPU 可以将分配的二维数组保持在快速访问中内存缓存,差异仅与乘法次数有关。
当矩阵变大时,CPU 无法再将所有分配的内存保留在其内部缓存中,我们将开始看到缓存未命中并从外部内存存储中获取内存,这总是很多慢一点。
开销会随着矩阵大小的增加而增加。
【讨论】:
这可能是解决问题的绝佳方法,但我的问题是为什么需要更多时间?背后的逻辑。不管怎样,我会试试的。 这需要更长的时间,因为 1) 基于列的数组查找必须进行 4 亿次乘法运算,而基于行的运算需要 20.000 次。 2) 基于行的内存访问是顺序的,需要更少的内存读取和 CPU 缓存失效。以上是关于为啥在 C# 中的二维数组中按列写入速度很慢的主要内容,如果未能解决你的问题,请参考以下文章
Python使用numpy函数hsplit水平(按列)拆分numpy数组(返回拆分后的numpy数组列表)实战:水平(按列)拆分二维numpy数组split函数水平(按列)拆分二维numpy数组