【问题标题】:C# Dynamic jagged array iterationC# 动态交错数组迭代
【发布时间】:2012-02-18 23:24:12
【问题描述】:

对于这个问题的长度,我提前道歉......这有点牵扯。 我正在编写一个非常简单的“加权和”运算;取 n 张图像,将每个图像乘以特定的乘数,然后将它们相加成输出图像(通过迭代每个像素)。当图像数量不变时,我可以将逻辑硬编码到一次迭代中,但是,我希望使该方法足够灵活以处理可变数量的图像。我无法想出一个同样“高效”的方式来实现这一点,例如当输入数量未知时,无需额外的内部循环。这是我的情况:

var rnd = new Random();

//Pixels in input and output images
const int x = 1000000;

//An output composite image
var pixelmap = new int[x];

var tStart = DateTime.Now;

//Known number of inputs
int knownNumberOfInputs = 3;

//Weights to apply to each pixel of the input images
//multipliers[0] applies to all pixels of inputA,
//multipliers[1] applies to all pixels of inputB etc.
var multipliers = new byte[3];
rnd.NextBytes(multipliers);

/* situation 1 
* - I know how many input images
* - Arrays are independent */

//3 (knownNumberOfInputs) input images (we'll use random numbers for filler)
var inputA = new byte[x];
rnd.NextBytes(inputA);
var inputB = new byte[x];
rnd.NextBytes(inputB);
var inputC = new byte[x];
rnd.NextBytes(inputC);

//I can iterate through each pixel of each input image, multiply and sum for pixelmap value.
//Without a nested loop
for (var i = 0; i < x; i++)
{
    pixelmap[i] = (
        (inputA[i]*multipliers[0]) +
        (inputB[i]*multipliers[1]) +
        (inputC[i]*multipliers[2])
                        );
}

Console.WriteLine("Operation took " + DateTime.Now.Subtract(tStart).TotalMilliseconds + " ms");
// Operation took 39 ms

tStart = DateTime.Now;

/* situation 2
* unknown number of inputs
* inputs are contained within jagged array */

/* Caveat - multipliers.Length == inputs.Length */

//var unknownNumberOfInputs = rnd.Next(1, 10);
var unknownNumberOfInputs = 3; //Just happens to be the same number (for performance comparisons)

multipliers = new byte[unknownNumberOfInputs];
rnd.NextBytes(multipliers);

//Jagged array to contain input images
var inputs = new byte[unknownNumberOfInputs][];

//Load unknownNumberOfInputs of input images into jagged array
for (var i = 0; i < unknownNumberOfInputs; i++)
{
    inputs[i] = new byte[x];
    rnd.NextBytes(inputs[i]);
}

// I cannot iterate through each pixel of each input image
// Inner nested loop
for (var i = 0; i < x; i++)
{
    for (var t=0;t<multipliers.Length;t++)
    {
        pixelmap[i] += (inputs[t][i] * multipliers[t]);
    }
}

Console.WriteLine("Operation took " + DateTime.Now.Subtract(tStart).TotalMilliseconds + " ms");
//Operation took 54 ms

//How can I get rid of the inner nested loop and gain the performance of LoopA?
//Or is that the cost of not knowing?

大起大落

更多信息

  • pixelmap 将进入 Silverlight 中的 WriteableBitmap - 一旦构建,它将采用 1D 数组作为像素源(因为高度/宽度被传递给构造函数)
  • 每个输入图像都有一个特定的乘数,例如将输入 1 的所有像素乘以 2,将输入 2 的所有像素乘以 3,等等。
  • 输入永远不会超过 20 个。

【问题讨论】:

  • 这很难理解。但是,如果您将事情计时到毫秒,您应该使用 Stopwatch 类而不是 DateTime
  • 此外,您应该排除从您的时间生成测试数据。
  • 我意识到这很难理解......我花了一段时间才提出这个问题。我将使用秒表。

标签: c# performance dynamic loops jagged-arrays


【解决方案1】:

您应该尝试交换内循环和外循环。

您的像素图可能适合 Cpu 缓存,然后多次写入它不会造成太大伤害。

然后,您可以展开循环遍历像素以获得最佳性能的内部循环。请务必在调试器之外测试发布版本以获得正确的时序。

如果您仍然不满意,您可以一次计算一条图像扫描线。

【讨论】:

    【解决方案2】:

    我建议您创建一个表示您的计算的表达式。然后编译那个表达式。

    您的表达式将是一个 lambda。三个输入的示例:

    void (byte[] inputA, byte[] inputB, byte[] inputC) {
    for (var i = 0; i < x; i++)
    {
        pixelmap[i] = (
            (inputA[i]*multipliers0) +
            (inputB[i]*multipliers1) +
            (inputC[i]*multipliers1)
                            );
    }
    }
    

    使用 .NET 4,您可以将 for 循环用作表达式(不在 .NET 2 中)。

    听起来很难,但实际上很容易做到。

    澄清一下:您将在运行时编译一个专用于固定数量输入的函数。

    您甚至可以玩一些技巧,例如将循环展开 2 或 4 次。您还可以像在示例中那样将乘数内联为常量。这将比嵌套循环快得多。

    请注意,循环位于表达式树内部,而不是围绕它。这意味着您只有一个委托调用(以及可重用编译结果)的开销。

    这里有一些示例代码可以帮助您入门:

    int inputCount = ...;
    var paramExpressions = GenerateArray(inputCount, i => Expression.Parameter(typeof(byte[]), "input" + i);
    
    var summands = GenerateArray(inputCount, i => Expression.Mul(/* inputA[i] HERE */, Expression.Constant(multipliers[i]));
    
    var sum = summands.Aggregate((a,b) => Expression.Add(a,b));
    
    var assignment = /* assign sum to pixelmap[i] here */;
    
    var loop = /* build a loop. ask a new question to find out how to do this, or use google */
    
    var lambda = Expression.Lambda(paramExpressions, loop);
    
    var delegate = lambda.Compile();
    
    //you are done compiling. now invoke:
    
    delegate.DynamicInvoke(arrayOfInputs); //send an object of type byte[][] into the lambda
    

    就是这样。你需要填补空白。

    【讨论】:

    • 所以每个职位都有一个功能。输入数?==20 个编译函数?没想到这个……
    • 是的。您可以懒惰地执行此操作,就像需要功能一样。请记住,您甚至可以专门针对具体乘法器,以便将它们编译为 CPU 的立即加载指令。
    • 我觉得这是正确的答案(尽管我从其他人那里学到了一些东西),但它确实超出了我的想象。我想我可以弄清楚如何在运行时编译一个表达式,但我不清楚如何动态地将参数的数量更改为表达式......该示例在签名中硬编码了 3 个参数。我也不清楚将乘数内联为常量是什么意思。在我看到曙光之前,我可能会实施您的其他答案。
    • 我添加了示例代码。我认为最困难的部分现在已经清楚了。不要害怕自己尝试!做编译器工作很有趣。
    • 非常感谢您的回复,但是,唉,超出了我的能力范围。我不知道 GenerateArray 是什么,它似乎缺少一些括号。我打算把它放在一边以备不时之需。
    【解决方案3】:

    您可以通过三种方式提高此代码的性能:

    1. 切换ti 循环。这样,您将使用相同的 2 个大数组,并且可以应用第 2 项:
    2. 使用临时变量消除内部循环中的数组访问。
    3. 使用一种称为“循环展开”的技术:以 3 个为一组进行计算,直到剩下的输入少于 3 个。然后再循环一次。

    这就是这一切的样子:

    int t = 0;
    for (; t < multipliers.Length - 2; t += 3) {
        var input1 = inputs[t];
        var input2 = inputs[t+1];
        var input3 = inputs[t+2];
        var multiplier1 = multipliers[t];
        var multiplier2 = multipliers[t+1];
        var multiplier3 = multipliers[t+2];
        if (t == 0) {
            for (var i = 0; i < x; i++)
                pixelmap[i] = input1[i] * multiplier1
                    + input2[i] * multiplier2
                    + input3[i] * multiplier3;
        } else {
            for (var i = 0; i < x; i++)
                pixelmap[i] += input1[i] * multiplier1
                    + input2[i] * multiplier2
                    + input3[i] * multiplier3;
        }
    }
    if (multipliers.Length < 3)
        Array.Clear(pixelmap, 0, pixelmap.Length);
    for (; t < multipliers.Length; t++) {
        var input = inputs[t];
        var multiplier = multipliers[t];
        for (var i = 0; i < x; i++)
            pixelmap[i] += input[i] * multiplier;
    }
    

    我还会对您计算结果的方式进行一些更改:

    1. 您似乎是在 Visual Studio 中运行基准测试,可能处于调试模式。确保在发布模式下在 Visual Studio 之外运行基准测试。
    2. 仅对您感兴趣的代码进行基准测试。不要使用创建测试数据的代码。
    3. Stopwatch 类非常适合用于计时目的,尤其是对于非常短的时间跨度。

    【讨论】:

      【解决方案4】:

      这是另一个答案:使用 T4 模板为 1 到 20 个输入生成所有可能的函数作为编译时间。这不像我之前的回答那么酷,但也很好用。

      【讨论】:

        猜你喜欢
        • 2010-09-27
        • 2018-02-02
        • 1970-01-01
        • 2019-03-25
        • 2017-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多