簡體   English   中英

如何在C#中快速從另一個中減去一個ushort數組?

[英]How to quickly subtract one ushort array from another in C#?

我需要從ushort arrayB中具有相同長度的相應索引值中快速減去ushort arrayA中的每個值。

另外,如果差異為負,我需要存儲零,而不是負差。

(確切地說,長度= 327680,因為我從另一個相同大小的圖像中減去640x512圖像)。

下面的代碼目前需要大約20ms,如果可能的話,我想在~5ms內將其降低。 不安全的代碼是可以的,但請提供一個例子,因為我不擅長編寫不安全的代碼。

謝謝!

public ushort[] Buffer { get; set; }

public void SubtractBackgroundFromBuffer(ushort[] backgroundBuffer)
{
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    sw.Start();

    int bufferLength = Buffer.Length;

    for (int index = 0; index < bufferLength; index++)
    {
        int difference = Buffer[index] - backgroundBuffer[index];

        if (difference >= 0)
            Buffer[index] = (ushort)difference;
        else
            Buffer[index] = 0;
    }

    Debug.WriteLine("SubtractBackgroundFromBuffer(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
}

更新:雖然它不是嚴格的C#,為了其他人的利益,我終於最終使用以下代碼將C ++ CLR類庫添加到我的解決方案中。 它運行在~3.1ms。 如果使用非托管C ++庫,則運行時間約為2.2毫秒。 由於時差很小,我決定使用托管庫。

// SpeedCode.h
#pragma once
using namespace System;

namespace SpeedCode
{
    public ref class SpeedClass
    {
        public:
            static void SpeedSubtractBackgroundFromBuffer(array<UInt16> ^ buffer, array<UInt16> ^ backgroundBuffer, int bufferLength);
    };
}

// SpeedCode.cpp
// This is the main DLL file.
#include "stdafx.h"
#include "SpeedCode.h"

namespace SpeedCode
{
    void SpeedClass::SpeedSubtractBackgroundFromBuffer(array<UInt16> ^ buffer, array<UInt16> ^ backgroundBuffer, int bufferLength)
    {
        for (int index = 0; index < bufferLength; index++)
        {
            buffer[index] = (UInt16)((buffer[index] - backgroundBuffer[index]) * (buffer[index] > backgroundBuffer[index]));
        }
    }
}

然后我稱之為:

    public void SubtractBackgroundFromBuffer(ushort[] backgroundBuffer)
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        SpeedCode.SpeedClass.SpeedSubtractBackgroundFromBuffer(Buffer, backgroundBuffer, Buffer.Length);

        Debug.WriteLine("SubtractBackgroundFromBuffer(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));
    }

一些基准。

  1. SubtractBackgroundFromBuffer:這是問題的原始方法。
  2. SubtractBackgroundFromBufferWithCalcOpt:這是用TTat提高計算速度的原始方法。
  3. SubtractBackgroundFromBufferParallelFor:來自Selman22答案的解決方案。
  4. SubtractBackgroundFromBufferBlockParallelFor:我的回答。 與3.類似,但將處理分為4096個值的塊。
  5. SubtractBackgroundFromBufferPartitionedParallelForEach: Geoff的第一個答案。
  6. SubtractBackgroundFromBufferPartitionedParallelForEachHack: Geoff的第二個答案。

更新

有趣的是,我可以通過使用(如Bruno Costa所建議的)為SubtractBackgroundFromBufferBlockParallelFor獲得小幅度的增加(~6%)

Buffer[i] = (ushort)Math.Max(difference, 0);

代替

if (difference >= 0)
    Buffer[i] = (ushort)difference;
else
    Buffer[i] = 0;

結果

請注意,這是每次運行中1000次迭代的總時間。

SubtractBackgroundFromBuffer(ms):                                 2,062.23 
SubtractBackgroundFromBufferWithCalcOpt(ms):                      2,245.42
SubtractBackgroundFromBufferParallelFor(ms):                      4,021.58
SubtractBackgroundFromBufferBlockParallelFor(ms):                   769.74
SubtractBackgroundFromBufferPartitionedParallelForEach(ms):         827.48
SubtractBackgroundFromBufferPartitionedParallelForEachHack(ms):     539.60

因此,從這些結果看來, 最佳方法結合了小增益的計算優化和利用Parallel.For來操作圖像的塊。 您的里程當然會有所不同,並行代碼的性能對您運行的CPU很敏感。

測試線束

我在發布模式下為每個方法運行了這個。 我這樣開始並停止Stopwatch以確保只測量處理時間。

System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
ushort[] bgImg = GenerateRandomBuffer(327680, 818687447);

for (int i = 0; i < 1000; i++)
{
    Buffer = GenerateRandomBuffer(327680, 128011992);                

    sw.Start();
    SubtractBackgroundFromBuffer(bgImg);
    sw.Stop();
}

Console.WriteLine("SubtractBackgroundFromBuffer(ms): " + sw.Elapsed.TotalMilliseconds.ToString("N2"));


public static ushort[] GenerateRandomBuffer(int size, int randomSeed)
{
    ushort[] buffer = new ushort[327680];
    Random random = new Random(randomSeed);

    for (int i = 0; i < size; i++)
    {
        buffer[i] = (ushort)random.Next(ushort.MinValue, ushort.MaxValue);
    }

    return buffer;
}

方法

public static void SubtractBackgroundFromBuffer(ushort[] backgroundBuffer)
{
    int bufferLength = Buffer.Length;

    for (int index = 0; index < bufferLength; index++)
    {
        int difference = Buffer[index] - backgroundBuffer[index];

        if (difference >= 0)
            Buffer[index] = (ushort)difference;
        else
            Buffer[index] = 0;
    }
}

public static void SubtractBackgroundFromBufferWithCalcOpt(ushort[] backgroundBuffer)
{
    int bufferLength = Buffer.Length;

    for (int index = 0; index < bufferLength; index++)
    {
        if (Buffer[index] < backgroundBuffer[index])
        {
            Buffer[index] = 0;
        }
        else
        {
            Buffer[index] -= backgroundBuffer[index];
        }
    }
}

public static void SubtractBackgroundFromBufferParallelFor(ushort[] backgroundBuffer)
{
    Parallel.For(0, Buffer.Length, (i) =>
    {
        int difference = Buffer[i] - backgroundBuffer[i];
        if (difference >= 0)
            Buffer[i] = (ushort)difference;
        else
            Buffer[i] = 0;
    });
}        

public static void SubtractBackgroundFromBufferBlockParallelFor(ushort[] backgroundBuffer)
{
    int blockSize = 4096;

    Parallel.For(0, (int)Math.Ceiling(Buffer.Length / (double)blockSize), (j) =>
    {
        for (int i = j * blockSize; i < (j + 1) * blockSize; i++)
        {
            int difference = Buffer[i] - backgroundBuffer[i];

            Buffer[i] = (ushort)Math.Max(difference, 0);                    
        }
    });
}

public static void SubtractBackgroundFromBufferPartitionedParallelForEach(ushort[] backgroundBuffer)
{
    Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
        {
            for (int i = range.Item1; i < range.Item2; ++i)
            {
                if (Buffer[i] < backgroundBuffer[i])
                {
                    Buffer[i] = 0;
                }
                else
                {
                    Buffer[i] -= backgroundBuffer[i];
                }
            }
        });
}

public static void SubtractBackgroundFromBufferPartitionedParallelForEachHack(ushort[] backgroundBuffer)
{
    Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
    {
        for (int i = range.Item1; i < range.Item2; ++i)
        {
            unsafe
            {
                var nonNegative = Buffer[i] > backgroundBuffer[i];
                Buffer[i] = (ushort)((Buffer[i] - backgroundBuffer[i]) *
                    *((int*)(&nonNegative)));
            }
        }
    });
}

這是個有趣的問題。

只有在測試結果不是負數后才執行減法(如TTat和Maximum Cookie所建議的)影響可以忽略不計,因為這種優化已經可以由JIT編譯器執行。

並行化任務(如Selman22所建議的)是一個好主意,但是當循環速度與此情況一樣快時,開銷最終會超過增益,因此Selman22的實現在我的測試中實際運行得更慢。 我懷疑nick_w的基准是在附帶調試器情況下產生的,隱藏了這個事實。

在較大的塊中並行化任務(如nick_w所示 )處理開銷問題,並且實際上可以產生更快的性能,但您不必自己計算塊 - 您可以使用Partitioner為您執行此操作:

public static void SubtractBackgroundFromBufferPartitionedParallelForEach(
    ushort[] backgroundBuffer)
{
    Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
        {
            for (int i = range.Item1; i < range.Item2; ++i)
            {
                if (Buffer[i] < backgroundBuffer[i])
                {
                    Buffer[i] = 0;
                }
                else
                {
                    Buffer[i] -= backgroundBuffer[i];
                }
            }
        });
}

在我的測試中,上述方法始終優於nick_w的手卷組塊。

可是等等! 除此之外還有更多。

減慢代碼速度的真正罪魁禍首不是賦值或算術。 這是if語句。 它如何影響性能將受到您正在處理的數據性質的重大影響。

nick_w的基准測試為兩個緩沖區生成相同幅度的隨機數據。 但是,我懷疑你很可能在后台緩沖區中實際擁有較低的平均幅度數據。 由於分支預測,這個細節可能很重要(如本經典SO答案中所述 )。

當后台緩沖區中的值通常小於緩沖區中的值時,JIT編譯器會注意到這一點,並相應地優化該分支。 當每個緩沖區中的數據來自相同的隨機群體時,無法猜測if語句的結果,准確度大於50%。 正是后一種情況, nick_w是基准測試,在這些情況下,我們可以通過使用不安全的代碼將bool轉換為整數並避免分支來進一步優化您的方法。 (請注意,以下代碼依賴於bool如何在內存中表示的實現細節,雖然它適用於.NET 4.5中的場景,但它不一定是個好主意,並且在此處顯示用於說明目的。)

public static void SubtractBackgroundFromBufferPartitionedParallelForEachHack(
    ushort[] backgroundBuffer)
{
    Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
        {
            for (int i = range.Item1; i < range.Item2; ++i)
            {
                unsafe
                {
                    var nonNegative = Buffer[i] > backgroundBuffer[i];
                    Buffer[i] = (ushort)((Buffer[i] - backgroundBuffer[i]) *
                        *((int*)(&nonNegative)));
                }
            }
        });
}

如果您真的希望減少更多的時間,那么您可以通過將語言切換到C ++ / CLI以更安全的方式遵循此方法,因為這將允許您在算術表達式中使用布爾值而無需使用不安全的代碼:

UInt16 MyCppLib::Maths::SafeSubtraction(UInt16 minuend, UInt16 subtrahend)
{
    return (UInt16)((minuend - subtrahend) * (minuend > subtrahend));
}

您可以使用C ++ / CLI創建一個純托管的DLL,公開上面的靜態方法,然后在C#代碼中使用它:

public static void SubtractBackgroundFromBufferPartitionedParallelForEachCpp(
    ushort[] backgroundBuffer)
{
    Parallel.ForEach(Partitioner.Create(0, Buffer.Length), range =>
    {
        for (int i = range.Item1; i < range.Item2; ++i)
        {
            Buffer[i] = 
                MyCppLib.Maths.SafeSubtraction(Buffer[i], backgroundBuffer[i]);
        }
    });
}

這比上面的hacky不安全的C#代碼更勝一籌。 事實上,它是如此之快,你可以使用C ++ / CLI編寫整個方法忘記並行化,它仍然會勝過其他技術。

使用nick_w的測試工具 ,上述方法將勝過迄今為止發布的任何其他建議。 以下是我得到的結果(1-4是他試過的案例,5-7是這個答案中概述的案例):

1. SubtractBackgroundFromBuffer(ms):                               2,021.37
2. SubtractBackgroundFromBufferWithCalcOpt(ms):                    2,125.80
3. SubtractBackgroundFromBufferParallelFor(ms):                    3,431.58
4. SubtractBackgroundFromBufferBlockParallelFor(ms):               1,401.36
5. SubtractBackgroundFromBufferPartitionedParallelForEach(ms):     1,197.76
6. SubtractBackgroundFromBufferPartitionedParallelForEachHack(ms):   742.72
7. SubtractBackgroundFromBufferPartitionedParallelForEachCpp(ms):    499.27

但是 ,在我希望你實際擁有的場景中,背景值通常較小,成功的分支預測可以全面改善結果,並且避免if語句的'hack'實際上更慢:

當我將后台緩沖區中的值限制在0-6500范圍內0-6500 (c。緩沖區的10%),以下是使用nick_w的測試工具得到的結果:

1. SubtractBackgroundFromBuffer(ms):                                 773.50
2. SubtractBackgroundFromBufferWithCalcOpt(ms):                      915.91
3. SubtractBackgroundFromBufferParallelFor(ms):                    2,458.36
4. SubtractBackgroundFromBufferBlockParallelFor(ms):                 663.76
5. SubtractBackgroundFromBufferPartitionedParallelForEach(ms):       658.05
6. SubtractBackgroundFromBufferPartitionedParallelForEachHack(ms):   762.11
7. SubtractBackgroundFromBufferPartitionedParallelForEachCpp(ms):    494.12

您可以看到結果1-5已經大大改善,因為它們現在受益於更好的分支預測。 結果6和7沒有太大變化,因為他們避免了分支。

這種數據變化徹底改變了一切。 在這種情況下,即使是最快的所有C#解決方案現在只比原始代碼快15%。

底線 :務必使用代表性數據測試您選擇的任何方法,否則您的結果將毫無意義。

你可以試試Parallel.For

Parallel.For(0, Buffer.Length, (i) =>
{
    int difference = Buffer[i] - backgroundBuffer[i];
    if (difference >= 0)
          Buffer[i] = (ushort) difference;
    else
         Buffer[i] = 0;
}); 

更新:我已經嘗試了,我看到你的情況有一個微小的差別,但是當陣列變大時,差異也變大了

在此輸入圖像描述

在實際執行減法之前,首先檢查結果是否為負數,可能會略微提高性能。 這樣,如果結果為負,則不需要執行減法。 例:

if (Buffer[index] > backgroundBuffer[index])
    Buffer[index] = (ushort)(Buffer[index] - backgroundBuffer[index]);
else
    Buffer[index] = 0;

這是一個使用Zip()的解決方案:

Buffer = Buffer.Zip<ushort, ushort, ushort>(backgroundBuffer, (x, y) =>
{
    return (ushort)Math.Max(0, x - y);
}).ToArray();

它的表現不如其他答案,但它絕對是最短的解決方案。

關於什么,

Enumerable.Range(0, Buffer.Length).AsParalell().ForAll(i =>
    {
         unsafe
        {
            var nonNegative = Buffer[i] > backgroundBuffer[i];
            Buffer[i] = (ushort)((Buffer[i] - backgroundBuffer[i]) *
                *((int*)(&nonNegative)));
        }
    });

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM