【问题标题】:Parallelizing the histogram function并行化直方图函数
【发布时间】:2013-01-31 15:37:00
【问题描述】:

我已经实现了一个简单函数的正常并行版本,该函数从 32bppArgb 位图计算直方图。普通版本在 1920x1080 图像上大约需要 0.03 秒,而并行版本需要 0.07 秒。

线程开销真的那么重吗?除了 Parallel.For 之外还有其他构造可以加快这个过程吗?由于我正在处理 30fps 视频,因此我需要加快速度。

这是简化的代码:

public sealed class Histogram
{
    public int MaxA = 0;
    public int MaxR = 0;
    public int MaxG = 0;
    public int MaxB = 0;
    public int MaxT = 0;

    public int [] A = null;
    public int [] R = null;
    public int [] G = null;
    public int [] B = null;

    public Histogram ()
    {
        this.A = new int [256];
        this.R = new int [256];
        this.G = new int [256];
        this.B = new int [256];

        this.Initialize();
    }

    public void Initialize ()
    {
        this.MaxA = 0;
        this.MaxR = 0;
        this.MaxG = 0;
        this.MaxB = 0;
        this.MaxT = 0;

        for (int i = 0; i < this.A.Length; i++)
            this.A [i] = 0;
        for (int i = 0; i < this.R.Length; i++)
            this.R [i] = 0;
        for (int i = 0; i < this.G.Length; i++)
            this.G [i] = 0;
        for (int i = 0; i < this.B.Length; i++)
            this.B [i] = 0;
    }

    public void ComputeHistogram (System.Drawing.Bitmap bitmap, bool parallel = false)
    {
        System.Drawing.Imaging.BitmapData data = null;

        data = bitmap.LockBits
        (
            new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height),
            System.Drawing.Imaging.ImageLockMode.ReadOnly,
            System.Drawing.Imaging.PixelFormat.Format32bppArgb
        );

        try
        {
            ComputeHistogram(data, parallel);
        }
        catch
        {
            bitmap.UnlockBits(data);

            throw;
        }

        bitmap.UnlockBits(data);
    }

    public void ComputeHistogram (System.Drawing.Imaging.BitmapData data, bool parallel = false)
    {
        int stride = System.Math.Abs(data.Stride);

        this.Initialize();

        if (parallel)
        {
            unsafe
            {
                System.Threading.Tasks.Parallel.For
                (
                    0,
                    data.Height,
                    new System.Threading.Tasks.ParallelOptions() { MaxDegreeOfParallelism = System.Environment.ProcessorCount },
                    y =>
                    {
                        byte* pointer = ((byte*) data.Scan0) + (stride * y);

                        for (int x = 0; x < stride; x += 4)
                        {
                            this.B [pointer [x + 0]]++;
                            this.G [pointer [x + 1]]++;
                            this.R [pointer [x + 2]]++;
                            this.A [pointer [x + 3]]++;
                        }
                    }
                );
            }
        }
        else
        {
            unsafe
            {
                for (int y = 0; y < data.Height; y++)
                {
                    byte* pointer = ((byte*) data.Scan0) + (stride * y);

                    for (int x = 0; x < stride; x += 4)
                    {
                        this.B [pointer [x + 0]]++;
                        this.G [pointer [x + 1]]++;
                        this.R [pointer [x + 2]]++;
                        this.A [pointer [x + 3]]++;
                    }
                }
            }
        }

        for (int i = 0; i < this.A.Length; i++)
            if (this.MaxA < this.A [i]) this.MaxA = this.A [i];
        for (int i = 0; i < this.R.Length; i++)
            if (this.MaxR < this.R [i]) this.MaxR = this.R [i];
        for (int i = 0; i < this.G.Length; i++)
            if (this.MaxG < this.G [i]) this.MaxG = this.G [i];
        for (int i = 0; i < this.B.Length; i++)
            if (this.MaxB < this.B [i]) this.MaxB = this.B [i];

        if (this.MaxT < this.MaxA) this.MaxT = this.MaxA;
        if (this.MaxT < this.MaxR) this.MaxT = this.MaxR;
        if (this.MaxT < this.MaxG) this.MaxT = this.MaxG;
        if (this.MaxT < this.MaxB) this.MaxT = this.MaxB;
    }
}

【问题讨论】:

  • 您是否尝试过让每个线程计算的不仅仅是 1 行?可能让他们处理 10-20 可以加快一点。
  • 好吧,我已经用四个语句将运行 1920 次的循环分组。不知道如何构造它。有什么建议吗?
  • 对于传入Parallel.For 的lambda,尝试从y 循环到y+(您必须找到一些最佳数字)。当然,这意味着将Parallel.For的第二个参数从data.Height调整为别的。
  • 您如何确保对直方图存储桶的更新是原子的?您可能会从线程中获得序列化的性能。
  • 或者一个线程处理一个图像?

标签: c# .net image-processing parallel-processing histogram


【解决方案1】:

创建线程有相当大的开销。执行速度可能比单线程版本快得多,但完成速度太快,无法弥补这一初始开销。

如果你每帧都这样做,只会减慢你的速度。

但是,如果您手动创建线程池、手动分配工作并为每个帧重用线程,您可能会发现在第二或第三帧时您的代码会飞速超过单线程版本。

【讨论】:

    【解决方案2】:

    好吧,首先,您的并行循环中有一个巨大的错误:

    您将有多个线程访问、递增和更新共享数组 - 由于固有的竞争条件,只需在同一个图像上多次运行示例代码会导致截然不同的结果。

    但这不是你问的。

    至于为什么您发现使用并行实现会降低性能,简单的答案是您可能没有在每个并行任务的主体中做足够的工作来抵消创建新任务的“启动成本”,调度它等等。

    可能更关键的是,我相信您正在彻底摆脱 L1/L2 缓存,内存中的所有跳跃 - 每个任务线程都会尝试将它认为需要的内容加载到缓存内存中,但是当您在各处进行索引时,您不再创建一致的访问模式,因此每次尝试访问位图缓冲区或内部数组时,您都可能会遇到缓存未命中。

    还有一种同样高效的方法可以在不使用不安全代码的情况下获取位图的只读数据……实际上,让我们先这样做:

    因此,通过调用LockBits,您有一个指向非托管内存的指针。让我们复制它:

    System.Drawing.Imaging.BitmapData data = null;
    data = bitmap.LockBits
    (
        new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height),
        System.Drawing.Imaging.ImageLockMode.ReadOnly,
        System.Drawing.Imaging.PixelFormat.Format32bppArgb
    );
    
    // For later usage
    var imageStride = data.Stride;
    var imageHeight = data.Height;
    
    // allocate space to hold the data
    byte[] buffer = new byte[data.Stride * data.Height];
    
    // Source will be the bitmap scan data
    IntPtr pointer = data.Scan0;
    
    // the CLR marshalling system knows how to move blocks of bytes around, FAST.
    Marshal.Copy(pointer, buffer, 0, buffer.Length);
    
    // and now we can unlock this since we don't need it anymore
    bitmap.UnlockBits(data);
    
    ComputeHistogram(buffer, imageStride, imageHeight, parallel);
    

    现在,至于竞态条件 - 您可以通过使用 Interlocked 调用来提高计数以合理的性能方式克服这个问题(注意!!!多线程编程是困难的,我完全有可能这里的解决方案并不完美!

    public void ComputeHistogram (byte[] data, int stride, int height, bool parallel = false)
    {
        this.Initialize();
    
        if (parallel)
        {
            System.Threading.Tasks.Parallel.For
            (
                0,
                height,
                new ParallelOptions() { MaxDegreeOfParallelism = Environment.ProcessorCount },
                y =>
                {
                    int startIndex = (stride * y);
                    int endIndex = stride * (y+1);
                    for (int x = startIndex; x < endIndex; x += 4)
                    {
                        // Interlocked actions are more-or-less atomic 
                        // (caveats abound, but this should work for us)
                        Interlocked.Increment(ref this.B[data[x]]);
                        Interlocked.Increment(ref this.G[data[x+1]]);
                        Interlocked.Increment(ref this.R[data[x+2]]);
                        Interlocked.Increment(ref this.A[data[x+3]]);
                    }
                }
            );
        }
        else
        {
            // the original way is ok for non-parallel, since only one
            // thread is mucking around with the data
        }
    
        // Sorry, couldn't help myself, this just looked "cleaner" to me
        this.MaxA = this.A.Max();
        this.MaxR = this.R.Max();
        this.MaxG = this.G.Max();
        this.MaxB = this.B.Max();
        this.MaxT = new[] { this.MaxA, this.MaxB, this.MaxG, this.MaxR }.Max();
    }
    

    那么,这对运行时行为有什么影响?

    不是很多,但至少并行分叉现在计算出正确的结果。 :)

    使用非常便宜的测试台:

    void Main()
    {    
        foreach(var useParallel in new[]{false, true})
        {
            var totalRunTime = TimeSpan.Zero;
            var sw = new Stopwatch();
            var runCount = 10;
            for(int run=0; run < runCount; run++)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Reset();
                sw.Start();
                var bmp = Bitmap.FromFile(@"c:\temp\banner.bmp") as Bitmap;
                var hist = new Histogram();
                hist.ComputeHistogram(bmp, useParallel);
                sw.Stop();
                totalRunTime = totalRunTime.Add(sw.Elapsed);
            }
            Console.WriteLine("Parallel={0}, Avg={1} ms", useParallel, totalRunTime.TotalMilliseconds / runCount);
        }
    }
    

    我得到这样的结果:

    Parallel=False, Avg=1.69777 ms
    Parallel=True, Avg=5.33584 ms
    

    如您所见,我们仍未解决您最初的问题。 :)

    所以让我们努力让并行工作“更好”:

    让我们看看“给任务更多的工作”做了什么:

    if (parallel)
    {
        var batchSize = 2;
        System.Threading.Tasks.Parallel.For
        (
            0,
            height / batchSize,
            new ParallelOptions() { MaxDegreeOfParallelism = Environment.ProcessorCount },
            y =>
            {
                int startIndex = (stride * y * batchSize);
                int endIndex = startIndex + (stride * batchSize);
                for (int x = startIndex; x < endIndex; x += 4)
                {
                    // Interlocked actions are more-or-less atomic 
                    // (caveats abound, but this should work for us)
                    Interlocked.Increment(ref this.B[data[x]]);
                    Interlocked.Increment(ref this.G[data[x+1]]);
                    Interlocked.Increment(ref this.R[data[x+2]]);
                    Interlocked.Increment(ref this.A[data[x+3]]);
                }
            }
        );
    }
    

    结果:

    Parallel=False, Avg=1.70273 ms
    Parallel=True, Avg=4.82591 ms
    

    哦,这看起来很有希望...我想知道当我们改变 batchSize 时会发生什么?

    让我们这样改变我们的测试台:

    void Main()
    {    
        foreach(var useParallel in new[]{false, true})
        {
            for(int batchSize = 1; batchSize < 1024; batchSize <<= 1)
            {
                var totalRunTime = TimeSpan.Zero;
                var sw = new Stopwatch();
                var runCount = 10;
                for(int run=0; run < runCount; run++)
                {
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.Collect();
                    sw.Reset();
                    sw.Start();
                    var bmp = Bitmap.FromFile(@"c:\temp\banner.bmp") as Bitmap;
                    var hist = new Histogram();
                    hist.ComputeHistogram(bmp, useParallel, batchSize);
                    sw.Stop();
                    totalRunTime = totalRunTime.Add(sw.Elapsed);
                }
                Console.WriteLine("Parallel={0}, BatchSize={1} Avg={2} ms", useParallel, batchSize, totalRunTime.TotalMilliseconds / runCount);
            }        
        }
    }
    

    结果:(只显示parallel=true,因为non-parallel不会改变)

    Parallel=True, BatchSize=1 Avg=5.57644 ms
    Parallel=True, BatchSize=2 Avg=5.49982 ms
    Parallel=True, BatchSize=4 Avg=5.20434 ms
    Parallel=True, BatchSize=8 Avg=5.1721 ms
    Parallel=True, BatchSize=16 Avg=5.00405 ms
    Parallel=True, BatchSize=32 Avg=4.44973 ms
    Parallel=True, BatchSize=64 Avg=2.28332 ms
    Parallel=True, BatchSize=128 Avg=1.39957 ms
    Parallel=True, BatchSize=256 Avg=1.29156 ms
    Parallel=True, BatchSize=512 Avg=1.28656 ms
    

    一旦我们达到 64-128 的批量大小范围,我们似乎正在接近某种渐近线,尽管当然您的里程可能会因您的位图大小等而有所不同。

    我希望这会有所帮助!这是我等待生产构建完成的一天的有趣分心! :)

    【讨论】:

    • 谢谢!像这样的答案具有传染性,并鼓励 SO'ers 回答更多问题。太棒了。
    • 关于 memcopy,我认为您这样做只是为了避免不安全的代码,对吧?
    • 我想知道是否有一种方法可以根据图像大小以编程方式计算最佳批量大小。当然,您可以使用启发式方法,但这不能很好地移植到不同的机器上。或者使用与您类似的测试台在另一个线程中进行运行时调整。
    • Heya @RaheelKhan - 很高兴你发现它有用!是的,试图避免unsafe 电话是其中之一。另一个试图减少您需要在位图上获取/锁定的长度。至于最佳批量大小?我纯粹是在猜测,但我想在最佳批量大小、像素扫描线的大小(即 4 字节 * 宽度或 ~stride)和 L1/L2 的大小之间存在关系/(L3)缓存。
    猜你喜欢
    • 1970-01-01
    • 2014-12-22
    • 2018-03-06
    • 2016-05-04
    • 2020-04-01
    • 2023-03-18
    • 2010-12-24
    • 2023-03-12
    • 1970-01-01
    相关资源
    最近更新 更多