C中的快速2D卷积

时间:2016-06-29 16:18:59

标签: python c algorithm performance optimization

我试图在Python中实现卷积神经网络。最初,我使用scipy.signal的convolve2d函数来进行卷积,但是它有很多开销,而且在C中实现我自己的算法并从python中调用它会更快,因为我知道是什么我的输入看起来像。

我已经实现了2个功能:

  1. 使用不可分离的内核卷积矩阵
  2. 用可分离的内核对矩阵进行卷积(现在我假设python在将它传递给C之前进行等级检查和拆分)
  3. 这两个函数都没有填充,因为我需要降低维数。

    不可分离的2D卷积

    // a - 2D matrix (as a 1D array), w - kernel
    double* conv2(double* a, double* w, double* result)
    {
        register double acc;
        register int i; 
        register int j;
        register int k1, k2;
        register int l1, l2;
        register int t1, t2;
    
        for(i = 0; i < RESULT_DIM; i++) 
        {
            t1 = i * RESULT_DIM; // loop invariants
            for(j = 0; j < RESULT_DIM; j++) 
            {   
                acc = 0.0;
                for(k1 = FILTER_DIM - 1, k2 = 0; k1 >= 0; k1--, k2++)
                {
                    t2 = k1 * FILTER_DIM;  // loop invariants
                    for(l1 = FILTER_DIM - 1, l2 = 0; l1 >= 0; l1--, l2++)
                    {
                        acc += w[t2 + l1] * a[(i + k2) * IMG_DIM + (j + l2)];
                    }
                }
                result[t1 + j] = acc;
            }
        }
    
        return result;
    }
    

    可分离的2D卷积

    // a - 2D matrix, w1, w2 - the separated 1D kernels
    double* conv2sep(double* a, double* w1, double* w2, double* result)
    {
        register double acc;
        register int i; 
        register int j;
        register int k1, k2;
        register int t;
        double* tmp = (double*)malloc(IMG_DIM * RESULT_DIM * sizeof(double));
    
        for(i = 0; i < RESULT_DIM; i++) // convolve with w1 
        {
            t = i * RESULT_DIM;
            for(j = 0; j < IMG_DIM; j++)
            {
                acc = 0.0;
                for(k1 = FILTER_DIM - 1, k2 = 0; k1 >= 0; k1--, k2++)
                {
                    acc += w1[k1] * a[k2 * IMG_DIM + t + j];
                }
                tmp[t + j] = acc;
            }
        }
    
        for(i = 0; i < RESULT_DIM; i++) // convolve with w2
        {
            t = i * RESULT_DIM;
            for(j = 0; j < RESULT_DIM; j++)
            {
                acc = 0.0;
                for(k1 = FILTER_DIM - 1, k2 = 0; k1 >= 0; k1--, k2++)
                {
                    acc += w2[k1] * tmp[t + (j + k2)];
                }
    
                result[t + j] = acc;
            }
        }
    
        free(tmp);
        return result;
    }
    

    使用gcc&#39; s -O3标志进行编译并在2.7GHz Intel i7上进行测试,使用4000x4000矩阵和5x5内核,我分别得到(平均5):

    271.21900 ms
    127.32000 ms
    

    这仍然比scipy.signal的convolve2d有了相当大的改进,相同操作大约需要2秒钟,但我需要更多的速度,因为我会调用这个函数数千次。将数据类型更改为浮动目前不是一个选项,即使它会导致相当大的加速。

    有没有办法可以进一步优化这些算法?我可以应用任何缓存技巧或例程来加快速度吗?

    任何建议都将不胜感激。

1 个答案:

答案 0 :(得分:2)

如果您只在x86上运行,请考虑使用SSE或AVX SIMD优化。对于double数据,吞吐量的改善将是适度的,但如果您可以切换到float那么您可以使用SSE或使用AVX获得大约4倍的改进。关于StackOverflow上的这个主题,有很多问题和答案,您可以从中获得有关实现的一些想法。或者,也存在许多可用的库,其包括高性能2D卷积(过滤)例程,并且这些例程通常利用SIMD来实现性能,例如,英特尔的IPP(商业)或OpenCV(免费)。

另一种可能性是利用多个核心 - 将您的图像拆分为块并在其自己的线程中运行每个块。例如。如果你有一个4核CPU,那么将你的图像分成4个块。 (见pthreads)。

如果您真的想要完全优化此操作,您当然可以结合上述两个想法。

您可以应用于当前代码以及任何未来实现(例如SIMD)的一些小优化:

  • 如果您的内核是对称的(或奇数对称的),那么您可以通过添加(减去)对称输入值并执行一次乘法而不是两次来减少操作次数

  • 对于可分离的情况,而不是分配一个完整的帧临时缓冲区,考虑使用“条带挖掘”方法 - 分配一个较小的缓冲区,这是一个全宽,但行数相对较少,然后处理你的图像在“条带”中,交替应用水平内核和垂直内核。这样做的好处是,您拥有更多缓存友好的访问模式和更小的内存占用。

关于编码风格的一些评论:

  • register关键字多年来一直是多余的,现代编译器会在您尝试使用它时发出警告 - 通过抛弃它来节省一些噪音(和一些打字)

  • 在C中投射malloc的结果是不受欢迎的 - 它是redundant and potentially dangerous

  • 创建任何输入参数const(即只读)并使用restrict表示任何永远不会混淆的参数(例如aresult) - 这不仅有助于避免编程错误(至少在const的情况下),但在某些情况下,它可以帮助编译器生成更好的优化代码(特别是在潜在别名指针的情况下)。