7

我正在寻找去/交错缓冲区的最快方法。更具体地说,我正在处理音频数据,因此我正在尝试优化我在拆分/组合通道和 FFT 缓冲区上所花费的时间。

目前,我正在为每个数组使用一个带有 2 个索引变量的 for 循环,因此只能进行加号操作,但所有托管数组检查都不会与 C 指针方法进行比较。

我喜欢 Buffer.BlockCopy 和 Array.Copy 方法,它们在我处理通道时节省了很多时间,但是数组没有办法拥有自定义索引器。

我试图找到一种方法来制作一个数组掩码,它将是一个带有自定义索引器的假数组,但在我的 FFT 操作中使用它时速度要慢两倍。我想编译器在直接访问数组时可以提取很多优化技巧,但无法优化通过类索引器访问。

我不想要一个不安全的解决方案,尽管从外观上看,这可能是优化此类操作的唯一方法。

谢谢。

这是我现在正在做的事情的类型:

private float[][] DeInterleave(float[] buffer, int channels)
{
    float[][] tempbuf = new float[channels][];
    int length = buffer.Length / channels;
    for (int c = 0; c < channels; c++)
    {
        tempbuf[c] = new float[length];
        for (int i = 0, offset = c; i < tempbuf[c].Length; i++, offset += channels)
            tempbuf[c][i] = buffer[offset];
    }
    return tempbuf;
}
4

5 回答 5

5

我进行了一些测试,这是我测试的代码:

delegate(float[] inout)
{ // My Original Code
    float[][] tempbuf = new float[2][];
    int length = inout.Length / 2;
    for (int c = 0; c < 2; c++)
    {
        tempbuf[c] = new float[length];
        for (int i = 0, offset = c; i < tempbuf[c].Length; i++, offset += 2)
            tempbuf[c][i] = inout[offset];
    }
}
delegate(float[] inout)
{ // jerryjvl's recommendation: loop unrolling
    float[][] tempbuf = new float[2][];
    int length = inout.Length / 2;
    for (int c = 0; c < 2; c++)
        tempbuf[c] = new float[length];
    for (int ix = 0, i = 0; ix < length; ix++)
    {
        tempbuf[0][ix] = inout[i++];
        tempbuf[1][ix] = inout[i++];
    }

}
delegate(float[] inout)
{ // Unsafe Code
    unsafe
    {
        float[][] tempbuf = new float[2][];
        int length = inout.Length / 2;
        fixed (float* buffer = inout)
            for (int c = 0; c < 2; c++)
            {
                tempbuf[c] = new float[length];
                float* offset = buffer + c;
                fixed (float* buffer2 = tempbuf[c])
                {
                    float* p = buffer2;
                    for (int i = 0; i < length; i++, offset += 2)
                        *p++ = *offset;
                }
            }
    }
}
delegate(float[] inout)
{ // Modifying my original code to see if the compiler is not as smart as i think it is.
    float[][] tempbuf = new float[2][];
    int length = inout.Length / 2;
    for (int c = 0; c < 2; c++)
    {
        float[] buf = tempbuf[c] = new float[length];
        for (int i = 0, offset = c; i < buf.Length; i++, offset += 2)
            buf[i] = inout[offset];
    }
}

和结果:(缓冲区大小 = 2^17,每次测试的迭代次数 = 200)

Average for test #1:      0.001286 seconds +/- 0.000026
Average for test #2:      0.001193 seconds +/- 0.000025
Average for test #3:      0.000686 seconds +/- 0.000009
Average for test #4:      0.000847 seconds +/- 0.000008

Average for test #1:      0.001210 seconds +/- 0.000012
Average for test #2:      0.001048 seconds +/- 0.000012
Average for test #3:      0.000690 seconds +/- 0.000009
Average for test #4:      0.000883 seconds +/- 0.000011

Average for test #1:      0.001209 seconds +/- 0.000015
Average for test #2:      0.001060 seconds +/- 0.000013
Average for test #3:      0.000695 seconds +/- 0.000010
Average for test #4:      0.000861 seconds +/- 0.000009

我每次测试都得到类似的结果。显然,不安全的代码是最快的,但令我惊讶的是,CLS 无法确定在处理锯齿状数组时它可以放弃索引检查。也许有人可以想出更多优化我的测试的方法。

编辑:我尝试使用不安全的代码展开循环,但没有效果。我还尝试优化循环展开方法:

delegate(float[] inout)
{
    float[][] tempbuf = new float[2][];
    int length = inout.Length / 2;
    float[] tempbuf0 = tempbuf[0] = new float[length];
    float[] tempbuf1 = tempbuf[1] = new float[length];

    for (int ix = 0, i = 0; ix < length; ix++)
    {
        tempbuf0[ix] = inout[i++];
        tempbuf1[ix] = inout[i++];
    }
}

结果也是命中与未命中比较测试#4,差异为 1%。到目前为止,测试#4 是我最好的方法。

正如我告诉 jerryjvl 的那样,问题是让 CLS 不索引检查输入缓冲区,因为添加第二个检查 (&& offset < inout.Length) 会减慢它的速度......

编辑 2:我之前在 IDE 中运行了测试,所以这里是外面的结果:

2^17 items, repeated 200 times
******************************************
Average for test #1:      0.000533 seconds +/- 0.000017
Average for test #2:      0.000527 seconds +/- 0.000016
Average for test #3:      0.000407 seconds +/- 0.000008
Average for test #4:      0.000374 seconds +/- 0.000008
Average for test #5:      0.000424 seconds +/- 0.000009

2^17 items, repeated 200 times
******************************************
Average for test #1:      0.000547 seconds +/- 0.000016
Average for test #2:      0.000732 seconds +/- 0.000020
Average for test #3:      0.000423 seconds +/- 0.000009
Average for test #4:      0.000360 seconds +/- 0.000008
Average for test #5:      0.000406 seconds +/- 0.000008


2^18 items, repeated 200 times
******************************************
Average for test #1:      0.001295 seconds +/- 0.000036
Average for test #2:      0.001283 seconds +/- 0.000020
Average for test #3:      0.001085 seconds +/- 0.000027
Average for test #4:      0.001035 seconds +/- 0.000025
Average for test #5:      0.001130 seconds +/- 0.000025

2^18 items, repeated 200 times
******************************************
Average for test #1:      0.001234 seconds +/- 0.000026
Average for test #2:      0.001319 seconds +/- 0.000023
Average for test #3:      0.001309 seconds +/- 0.000025
Average for test #4:      0.001191 seconds +/- 0.000026
Average for test #5:      0.001196 seconds +/- 0.000022

Test#1 = My Original Code
Test#2 = Optimized safe loop unrolling
Test#3 = Unsafe code - loop unrolling
Test#4 = Unsafe code
Test#5 = My Optimized Code

看起来循环展开是不利的。我的优化代码仍然是我最好的方法,与不安全的代码相比只有 10% 的差异。如果我能告诉编译器 (i < buf.Length) 意味着 (offset < inout.Length),它将放弃检查 (inout[offset]),我基本上会得到不安全的性能。

于 2009-06-07T13:55:46.977 回答
1

由于没有内置函数可以做到这一点,因此使用数组索引是您能想到的最快的操作。像这样的索引器和解决方案只会通过引入方法调用和阻止 JIT 优化器来优化边界检查而使事情变得更糟。

无论如何,我认为您当前的方法是unsafe您可以使用的最快的非解决方案。如果性能对您来说真的很重要(通常在信号处理应用程序中如此),您可以在unsafeC# 中完成所有事情(这足够快,可能与 C 相当)并将其包装在您从安全方法中调用的方法中.

于 2009-06-07T11:35:12.580 回答
1

它不会给您带来重大的性能提升(我在我的机器上大致测量了 20%),但您可以考虑针对常见情况进行一些循环展开。如果大多数时候您的频道数量相对有限:

static private float[][] Alternative(float[] buffer, int channels)
{
    float[][] result = new float[channels][];
    int length = buffer.Length / channels;
    for (int c = 0; c < channels; c++)
        result[c] = new float[length];

    int i = 0;
    if (channels == 8)
    {
        for (int ix = 0; ix < length; ix++)
        {
            result[0][ix] = buffer[i++];
            result[1][ix] = buffer[i++];
            result[2][ix] = buffer[i++];
            result[3][ix] = buffer[i++];
            result[4][ix] = buffer[i++];
            result[5][ix] = buffer[i++];
            result[6][ix] = buffer[i++];
            result[7][ix] = buffer[i++];
        }
    }
    else
        for (int ix = 0; ix < length; ix++)
            for (int ch = 0; ch < channels; ch++)
                result[ch][ix] = buffer[i++];


    return result;
}

只要您将一般后备变体保留在那里,它就会处理任意数量的通道,但如果它是展开的变体之一,您将获得速度提升。

于 2009-06-07T12:35:12.770 回答
1

也许在你自己的最佳答案中展开一些:

delegate(float[] inout)
{
    unsafe
    {
        float[][] tempbuf = new float[2][];
        int length = inout.Length / 2;

        fixed (float* buffer = inout)
        {
            float* pbuffer = buffer;

            tempbuf[0] = new float[length];
            tempbuf[1] = new float[length];

            fixed (float* buffer0 = tempbuf[0])
            fixed (float* buffer1 = tempbuf[1])
            {
                float* pbuffer0 = buffer0;
                float* pbuffer1 = buffer1;

                for (int i = 0; i < length; i++)
                {
                    *pbuffer0++ = *pbuffer++;
                    *pbuffer1++ = *pbuffer++;
                }
            }
        }
    }
}

这可能会获得更多的性能。

于 2009-06-07T14:23:00.807 回答
0

我想很多读者会质疑为什么你不想要音频处理等不安全的解决方案。这是一种需要热血优化的东西,如果知道它是通过虚拟机强制执行的,我个人会很不高兴。

于 2009-06-07T11:55:52.337 回答