【问题标题】:Make c# matrix code faster使 c# 矩阵代码更快
【发布时间】:2010-04-23 09:47:41
【问题描述】:

处理一些矩阵代码,我担心性能问题。

它是这样工作的:我有一个 IMatrix 抽象类(包含所有矩阵操作等),由 ColumnMatrix 类实现。

abstract class IMatrix
{
    public int Rows {get;set;}
    public int Columns {get;set;}
    public abstract float At(int row, int column);
}

class ColumnMatrix : IMatrix
{
    private data[];

    public override float At(int row, int column)
    {
        return data[row + columns * this.Rows];
    }
}

这个类在我的应用程序中被大量使用,但我担心性能问题。 仅针对相同大小的锯齿状数组测试读取 2000000x15 矩阵,我得到 1359ms 的数组访问和 9234ms 的矩阵访问:

public void TestAccess()
{
    int iterations = 10;

    int rows = 2000000;
    int columns = 15;
    ColumnMatrix matrix = new ColumnMatrix(rows, columns);
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < columns; j++)
            matrix[i, j] = i + j;

    float[][] equivalentArray = matrix.ToRowsArray();

    TimeSpan totalMatrix = new TimeSpan(0);
    TimeSpan totalArray = new TimeSpan(0);

    float total = 0f;
    for (int iteration = 0; iteration < iterations; iteration++)
    {
        total = 0f;
        DateTime start = DateTime.Now;
        for (int i = 0; i < rows; i++)
            for (int j = 0; j < columns; j++)
                total = matrix.At(i, j);
        totalMatrix += (DateTime.Now - start);

        total += 1f; //Ensure total is read at least once.
        total = total > 0 ? 0f : 0f;

        start = DateTime.Now;
        for (int i = 0; i < rows; i++)
            for (int j = 0; j < columns; j++)
                total = equivalentArray[i][j];
        totalArray += (DateTime.Now - start);
    }

    if (total < 0f)
        logger.Info("Nothing here, just make sure we read total at least once.");

    logger.InfoFormat("Average time for a {0}x{1} access, matrix : {2}ms", rows, columns, totalMatrix.TotalMilliseconds);
    logger.InfoFormat("Average time for a {0}x{1} access, array : {2}ms", rows, columns, totalArray.TotalMilliseconds);
    Assert.IsTrue(true);
}

所以我的问题是:我怎样才能让这件事变得更快?有什么办法可以让我的 ColumnMatrix.At 更快? 干杯!

【问题讨论】:

  • 矩阵数据是否需要保存在单个数组中?

标签: c# performance matrix


【解决方案1】:
  1. 删除abstract class IMatrix。这是错误的,因为它不是接口并且调用被覆盖的方法比调用 final(也就是非修饰方法)慢。
  2. 您可以使用不安全代码(指针)来获取数组元素,而无需进行数组边界检查(更快,但工作量更大且不安全)

【讨论】:

    【解决方案2】:

    您编写的数组代码可以很容易地优化,因为很明显您正在按顺序访问内存。这意味着 JIT 编译器在将其转换为本机代码方面可能会做得更好,这将带来更好的性能。
    您没有考虑的另一件事是内联仍然会遇到问题,所以如果您的 At 方法(为什么顺便说一句,不使用索引器属性?)没有内联,由于使用调用和堆栈操作,您将遭受巨大的性能损失。最后,您应该考虑密封 ColumnMatrix 类,因为这将使 JIT 编译器的优化更容易(call 肯定比 callvirt 更好)。

    【讨论】:

    • 密封类不允许 JIT 使用 call 而不是 callvirt。
    【解决方案3】:

    如果二维数组的性能要好得多,那你不使用二维数组作为你的类的内部存储,而不是使用计算索引开销的一维数组?

    【讨论】:

    • 因为我经常只需要访问矩阵的列,而 System.Array.Copy 似乎不存在于二维数组中...?
    • 二维数组比锯齿数组慢。我正在比较 4217x9 乘以 4217x9 的矩阵加法运算,锯齿状版本比二维一维快 5 倍。
    【解决方案4】:

    当您使用DateTime.Now 来衡量性能时,结果是相当随机的。时钟的分辨率大约是 1/20 秒,因此您不是在测量实际时间,而是在代码中测量时钟恰好滴答的位置。

    您应该改用Stopwatch 类,它的分辨率要高得多。

    【讨论】:

    • 那是错误的,除非他会使用 Win98。对于每个其他系统,分辨率约为 10 毫秒,这足以获得正确的时序。但即使是 1/20 也无所谓,因为他测量的时间约为 10 秒。
    【解决方案5】:

    对于元素的每次访问,您都需要进行乘法运算:行 + 列 * this.Rows。 您可能会看到内部是否也可以使用二维数组

    您还获得了额外的开销,即在类中将事物抽象出来。每次访问矩阵中的元素时,都会进行额外的方法调用

    【讨论】:

      【解决方案6】:

      改成这样:

      interface IMatrix
      {
          int Rows {get;set;}
          int Columns {get;set;}
          float At(int row, int column);
      }
      
      class ColumnMatrix : IMatrix
      {
          private data[,];
      
          public int Rows {get;set;}
          public int Columns {get;set;}
      
          public float At(int row, int column)
          {
              return data[row,column];
          }
      }
      

      你最好使用接口而不是抽象类 - 如果你需要它的通用功能,请为接口添加扩展方法。

      2D 矩阵也比锯齿状矩阵或扁平矩阵更快。

      【讨论】:

        【解决方案7】:

        您可以使用并行编程来加速您的算法。 您可以编译此代码,并比较正规矩阵方程(MultiplyMatricesSequential 函数)和并行矩阵方程(MultiplyMatricesParallel 函数)的性能。您已经实现了此方法的性能比较函数(在 Main 函数中)。

        您可以在 Visual Studio 2010 (.NET 4.0) 下编译此代码

        namespace MultiplyMatrices
        {
            using System;
            using System.Collections.Generic;
            using System.Collections.Concurrent;
            using System.Diagnostics;
            using System.Linq;
            using System.Threading;
            using System.Threading.Tasks;
        
            class Program
            {
                #region Sequential_Loop
                static void MultiplyMatricesSequential(double[,] matA, double[,] matB,
                                                        double[,] result)
                {
                    int matACols = matA.GetLength(1);
                    int matBCols = matB.GetLength(1);
                    int matARows = matA.GetLength(0);
        
                    for (int i = 0; i < matARows; i++)
                    {
                        for (int j = 0; j < matBCols; j++)
                        {
                            for (int k = 0; k < matACols; k++)
                            {
                                result[i, j] += matA[i, k] * matB[k, j];
                            }
                        }
                    }
                }
                #endregion
        
                #region Parallel_Loop
        
                static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result)
                {
                    int matACols = matA.GetLength(1);
                    int matBCols = matB.GetLength(1);
                    int matARows = matA.GetLength(0);
        
                    // A basic matrix multiplication.
                    // Parallelize the outer loop to partition the source array by rows.
                    Parallel.For(0, matARows, i =>
                    {
                        for (int j = 0; j < matBCols; j++)
                        {
                            // Use a temporary to improve parallel performance.
                            double temp = 0;
                            for (int k = 0; k < matACols; k++)
                            {
                                temp += matA[i, k] * matB[k, j];
                            }
                            result[i, j] = temp;
                        }
                    }); // Parallel.For
                }
        
                #endregion
        
        
                #region Main
                static void Main(string[] args)
                {
                    // Set up matrices. Use small values to better view 
                    // result matrix. Increase the counts to see greater 
                    // speedup in the parallel loop vs. the sequential loop.
                    int colCount = 180;
                    int rowCount = 2000;
                    int colCount2 = 270;
                    double[,] m1 = InitializeMatrix(rowCount, colCount);
                    double[,] m2 = InitializeMatrix(colCount, colCount2);
                    double[,] result = new double[rowCount, colCount2];
        
                    // First do the sequential version.
                    Console.WriteLine("Executing sequential loop...");
                    Stopwatch stopwatch = new Stopwatch();
                    stopwatch.Start();
        
                    MultiplyMatricesSequential(m1, m2, result);
                    stopwatch.Stop();
                    Console.WriteLine("Sequential loop time in milliseconds: {0}", stopwatch.ElapsedMilliseconds);
        
                    // For the skeptics.
                    OfferToPrint(rowCount, colCount2, result);
        
                    // Reset timer and results matrix. 
                    stopwatch.Reset();
                    result = new double[rowCount, colCount2];
        
                    // Do the parallel loop.
                    Console.WriteLine("Executing parallel loop...");
                    stopwatch.Start();
                    MultiplyMatricesParallel(m1, m2, result);
                    stopwatch.Stop();
                    Console.WriteLine("Parallel loop time in milliseconds: {0}", stopwatch.ElapsedMilliseconds);
                    OfferToPrint(rowCount, colCount2, result);
        
                    // Keep the console window open in debug mode.
                    Console.WriteLine("Press any key to exit.");
                    Console.ReadKey();
                }
        
        
                #endregion
        
                #region Helper_Methods
        
                static double[,] InitializeMatrix(int rows, int cols)
                {
                    double[,] matrix = new double[rows, cols];
        
                    Random r = new Random();
                    for (int i = 0; i < rows; i++)
                    {
                        for (int j = 0; j < cols; j++)
                        {
                            matrix[i, j] = r.Next(100);
                        }
                    }
                    return matrix;
                }
        
                private static void OfferToPrint(int rowCount, int colCount, double[,] matrix)
                {
                    Console.WriteLine("Computation complete. Print results? y/n");
                    char c = Console.ReadKey().KeyChar;
                    if (c == 'y' || c == 'Y')
                    {
                        Console.WindowWidth = 180;
                        Console.WriteLine();
                        for (int x = 0; x < rowCount; x++)
                        {
                            Console.WriteLine("ROW {0}: ", x);
                            for (int y = 0; y < colCount; y++)
                            {
                                Console.Write("{0:#.##} ", matrix[x, y]);
                            }
                            Console.WriteLine();
                        }
        
                    }
                }
        
                #endregion
            }
        
        }
        

        【讨论】:

        • 谢谢,我们的目标是衡量原始数据访问。我已经在用自制的Parallel.For,等待尽快切换到NET4。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-05-31
        • 2017-02-13
        • 2013-06-13
        • 2016-11-01
        • 1970-01-01
        • 2020-07-14
        相关资源
        最近更新 更多