c++ C#中的数组按位类型转换

jvidinwx  于 2023-07-01  发布在  C#
关注(0)|答案(1)|浏览(117)

下面是一个用C++编写的简单控制台应用程序:

#include <iostream>
using namespace std;

int main()
{
    const __int32 length = 4;
    __int32 ints[length] = {1, 2, 3, 4 };
    __int32* intArray = ints;
    __int64* longArray = (__int64*)intArray;
    for (__int32 i = 0; i < length; i++) cout << intArray[i] << '\n';
    cout << '\n';
    for (__int32 i = 0; i < length / 2; i++) cout << longArray[i] << '\n';
    cout << '\n';
    cout << "Press any key to exit.\n";
    cin.get();
}

该程序接受一个由4个32位有符号整数组成的数组,并将其转换为一个由2个64位有符号整数组成的数组。这是非常有效的,因为唯一的操作是将指针转换为不同的类型。
在C#中,可以通过创建目标类型的新数组并将原始数组的内存复制到目标数组来完成等效操作。这可以通过使用System.Runtime.InteropServices.Marshal类非常快速地实现。但是,由于复制许多兆字节数据的开销,这对于较大的阵列来说效率非常低。
此外,在某些情况下,可能希望不同非托管类型的两个数组引用内存中的同一位置。例如,在一个数组上执行操作并查看另一个数组中的更改。
需要明确的是,我希望逐位转换数组,而不是逐值转换。如果这没有意义,这是控制台的输出:

1
2
3
4

8589934593
17179869187
6yjfywim

6yjfywim1#

您可以使用Span<T>来执行此操作,而无需复制数组:

int[] source = { 1, 2, 3, 4 };

Span<long> dest = MemoryMarshal.Cast<int, long>(source.AsSpan());

foreach (var element in dest)
{
    Console.WriteLine(element); // Outputs 8589934593 and 17179869187
}

但是,如果您必须将数据作为数组,则必须最终制作副本。
如果你能接受不安全的代码,这可能会稍微快一点(但可能不会快到值得使用不安全的代码):

int[] source = { 1, 2, 3, 4 };

unsafe
{
    fixed (int* p = source)
    {
        long* q = (long*)p;

        for (int i = 0; i < source.Length/2; i++)
        {
            Console.WriteLine(*q++);
        }
    }
}

另一种方法(速度较慢,但会给予数据放在单独的数组中)是使用Buffer.BlockCopy()。如果您可以预先分配和重用目标数组,则可以保存分配目标的开销-但您仍然需要支付复制所有数据的费用。

int[] source = { 1, 2, 3, 4 };
long[] dest = new long[source.Length/2];

Buffer.BlockCopy(source, 0, dest, 0, sizeof(int) * source.Length);

foreach (var element in dest)
{
    Console.WriteLine(element);
}

我们永远不应该在没有基准的情况下做出性能决策,所以让我们尝试一些:

[MemoryDiagnoser]
public class Benchmarks
{
    [Benchmark]
    public void BlockCopy()
    {
        viaBlockCopy();
    }

    static long viaBlockCopy()
    {
        Buffer.BlockCopy(source, 0, dest, 0, sizeof(int) * source.Length);

        long total = 0;

        for (int i = 0; i < dest.Length; ++i)
            total += dest[i];

        return total;
    }

    [Benchmark]
    public void Unsafe()
    {
        viaUnsafe();
    }

    static long viaUnsafe()
    {
        unsafe
        {
            fixed (int* p = source)
            {
                long* q = (long*)p;
                long* end = q + source.Length / 2;

                long total  = 0;

                while (q != end)
                    total += *q++;

                return total;
            }
        }
    }

    [Benchmark]
    public void Span()
    {
        viaSpan();
    }

    static long viaSpan()
    {
        Span<long> result = MemoryMarshal.Cast<int, long>(source.AsSpan());

        long total = 0;

        foreach (var element in result)
        {
            total += element;
        }

        return total;
    }

    static readonly int[]  source = Enumerable.Range(0, 1024 * 1024).ToArray();
    static readonly long[] dest   = new long[1024 * 1024/2];
}

注意,BlockCopy()基准测试重用dest缓冲区,以避免创建输出数组的开销。如果您的代码必须为每个调用创建一个输出缓冲区,则速度会明显变慢。
结果是:

|    Method |     Mean |   Error |  StdDev | Allocated |
|---------- |---------:|--------:|--------:|----------:|
| BlockCopy | 362.7 us | 3.53 us | 3.30 us |         - |
|    Unsafe | 108.6 us | 0.68 us | 0.57 us |         - |
|      Span | 134.4 us | 0.37 us | 0.33 us |         - |

您可以自己决定不安全的代码是否值得额外的性能(就我个人而言,我完全避免不安全的代码)。
还要注意,这些基准测试包括迭代结果的所有元素的时间。如果省略了这部分,那么对于Spanunsafe方法,您最终只需要测量“转换”数据所需的少量时间。
为了完整起见,下面是从基准测试中删除total计算的时间(注意,数字的单位是纳秒而不是微秒!):

|    Method |            Mean |         Error |        StdDev | Allocated |
|---------- |----------------:|--------------:|--------------:|----------:|
| BlockCopy | 108,173.6835 ns | 1,591.5239 ns | 1,328.9946 ns |         - |
|    Unsafe |       0.9529 ns |     0.0105 ns |     0.0088 ns |         - |
|      Span |       1.1429 ns |     0.0042 ns |     0.0033 ns |         - |

现在你可以看到为什么我在总计算中加入了...

相关问题