【问题标题】:How to use fixed with a variable of type Array or T[]?如何将fixed 与Array 或T[] 类型的变量一起使用?
【发布时间】:2013-02-12 13:02:07
【问题描述】:

我正在开发一个IEqualityComparer,它应该能够非常快速地比较原始类型的数组。我的计划是获取指向数组和memcmp 的指针。像这样:

    public unsafe override bool Equals(T[] x, T[] y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x == null || y == null) return false;
        if (x.Length != y.Length) return false;

        var xArray = (Array)x;
        var yArray = (Array)y;
        fixed (void* xPtr = xArray) //compiler error 1
        fixed (T* yPtr = y) //compiler error 2
        {
            return memcmp(xPtr, yPtr, x.Length * this.elementSize);
        }
    }

固定语句不允许我固定ArrayT[]

有错误信息:

1. Cannot implicitly convert type 'System.Array' to 'void*'
2. Cannot take the address of, get the size of, or declare a pointer to a managed type ('T')

现在,我实际上并不关心我是如何完成这项工作的(我不致力于这种确切的方法)。我怎么知道memcmp 两个T[] T 是原始/blittable 类型?

我真的想避免切换类型并为每个有趣的类型创建一个专门的(和重复的)代码版本。由于性能限制,任何类型的反射解决方案都不可行(是的,我真的需要这里的性能 - 不适用过早的优化警告,因为它在 Stack Overflow 上是惯例)。

【问题讨论】:

  • 你不能从c#调用memcpy
  • 另外,你试过public unsafe override bool Equals(T[] x, T[] y) where T: struct吗?
  • 你为什么要优化你的代码?您可能会通过编组到非托管代码来产生更多性能问题。
  • p/invokes 有开销,这会抵消好处。你应该让 clr 完成它的工作。
  • 我敢打赌,使用 memcmp() 会显着加快大型数组的处理速度。固定指针的编组不应复制内存。例如,请参阅stackoverflow.com/questions/2031217/…

标签: c# fixed unsafe


【解决方案1】:

我知道 T 是一个原始/blittable 类型

你知道,编译器不知道。 CLR 要求垃圾收集器不能再移动固定对象中的所有内容。对于一个数组,它包括它的数组元素。唯一符合条件的 T 是一种简单的值类型,即 blittable。泛型没有给您一种将 T 限制为 blittable 类型的方法。

您通常会将 memcmp() 的参数声明为 byte[]。然后 pinvoke 编组器已经做了正确的事情,并将在调用 memcmp() 之前固定 byte[] 数组。但是,这也不起作用,因为您也无法轻松地将 T[] 转换为 byte[] 。您必须使用 GCHandle 固定自己。相应地将 memcmp() 参数声明为 IntPtr 而不是 byte[]。

实际上可以工作的类型子集足够小,可以考虑简单地编写方法重载而不是泛​​型方法。这现在使 pinvoke 编组器能够处理固定,相应地重载 memcmp() 函数声明。

【讨论】:

  • 你有没有提到fixed语句not将对象固定在内存中,即what它是supposed做的?
  • 只看语句生成的IL。
  • 非常有价值,汉斯。我有点知道 C# 编译器只使用托管指针(“内部指针”),但我从未意识到这意味着该对象是 not 固定的。我现在确信您编辑的最后一个语句是正确的设计选择。我会跟进此事。
  • 我会接受这个答案,因为它明确指出需要多个重载的 PInvoke 函数。我的想法似乎也没有解决方案,即在没有专门类型的情况下获得固定指针。
  • "fixed 语句实际上并没有固定对象。它创建了一个被垃圾收集器识别为引用的新指针。因此 GC 将更新该指针当它在压缩堆时移动数组时。”不对。 fixed 语句指示 GC 不要移动固定槽引用的内存。 所以记忆是固定在现实中的。
【解决方案2】:

您可以为每种要比较的数组编写单独的 P/Invoke 前端。我知道你并不是真的想专攻 T,但我认为开销不会太大。

这有点小题大做,因为我为同一个 API 函数定义了多个具有不同签名的 P/Invoke 方法,但通过这样做,我可以利用 P/Invoke 编组支持。

(请注意,只有当源数据确实是一个字节数组时,memcmp 的返回值的符号才有意义。如果你给它一个整数数组,你应该只将返回值与零进行比较并忽略它的符号。它所暗示的顺序对整数没有意义。)

例如,以下代码为我打印以下内容(RELEASE 构建,而不是调试构建):

MemCmp with ints took 00:00:08.0768666
ManagedMemCmp with ints took 00:00:10.3750453
MemCmp with bytes took 00:00:01.8740001
ManagedMemCmp with bytes took 00:00:09.2885763

请注意,byte[] 测试使用的是字节,因此比较的字节数是 int[] 测试使用的四分之一。托管代码进行相同数量的比较,因此相对要慢很多。

比较整数数组的时间并没有太大的差异,但是比较字节数组的时间差异很大。这向我表明,可能有一个托管优化,它使用 fixed 从字节数组中获取指向整数的指针,以便一次比较 4 个字节(在不适合 int 的结尾)。

我还认为您可以编写一个多线程托管版本(使用“int*”优化来比较字节数组),这将比非托管 memcmp() 快得多,后者当然不是多线程的(据我所知)。

无论如何,这是我的测试代码。请记住,发布构建,而不是调试!

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;


namespace Demo
{
    public static class Program
    {
        private static void Main(string[] args)
        {
            int[] a1 = new int[1000000];
            int[] a2 = new int[1000000];

            for (int i = 0; i < a1.Length-1; ++i)
            {
                a1[i] = i;
                a2[i] = i;
            }

            a1[a1.Length-1] = 1;
            a2[a1.Length-1] = 2;

            byte[] b1 = new byte[1000000];
            byte[] b2 = new byte[1000000];

            for (int i = 0; i < b1.Length-1; ++i)
            {
                b1[i] = (byte)i;
                b2[i] = (byte)i;
            }

            b1[a1.Length-1] = 1;
            b2[a1.Length-1] = 2;

            Stopwatch sw = Stopwatch.StartNew();
            testWithMemCmp(a1, a2);
            sw.Stop();
            Console.WriteLine("MemCmp with ints took " + sw.Elapsed);

            sw.Restart();
            testWithManagedMemCmp(a1, a2);
            sw.Stop();
            Console.WriteLine("ManagedMemCmp with ints took " + sw.Elapsed);

            sw.Restart();
            testWithMemCmp(b1, b2);
            sw.Stop();
            Console.WriteLine("MemCmp with bytes took " + sw.Elapsed);

            sw.Restart();
            testWithManagedMemCmp(b1, b2);
            sw.Stop();
            Console.WriteLine("ManagedMemCmp with bytes took " + sw.Elapsed);
        }

        private static void testWithMemCmp(int[] a1, int[] a2)
        {
            for (int j = 0; j < COUNT; ++j)
            {
                MemCmp(a1, a2);
            }
        }

        private static void testWithMemCmp(byte[] a1, byte[] a2)
        {
            for (int j = 0; j < COUNT; ++j)
            {
                MemCmp(a1, a2);
            }
        }

        private static void testWithManagedMemCmp(int[] a1, int[] a2)
        {
            for (int j = 0; j < COUNT; ++j)
            {
                ManagedMemCmp(a1, a2);
            }
        }

        private static void testWithManagedMemCmp(byte[] a1, byte[] a2)
        {
            for (int j = 0; j < COUNT; ++j)
            {
                ManagedMemCmp(a1, a2);
            }
        }

        public static bool ManagedMemCmp(int[] a1, int[] a2)
        {
            if (a1 == null || a2 == null || a1.Length != a2.Length)
            {
                throw new InvalidOperationException("Arrays are null or different lengths.");
            }

            for (int i = 0; i < a1.Length; ++i)
            {
                if (a1[i] != a2[i])
                {
                    return false;
                }
            }

            return true;
        }

        public static bool ManagedMemCmp(byte[] a1, byte[] a2)
        {
            if (a1 == null || a2 == null || a1.Length != a2.Length)
            {
                throw new InvalidOperationException("Arrays are null or different lengths.");
            }

            for (int i = 0; i < a1.Length; ++i)
            {
                if (a1[i] != a2[i])
                {
                    return false;
                }
            }


            return true;
        }

        public static bool MemCmp(byte[] a1, byte[] a2)
        {
            if (a1 == null || a2 == null || a1.Length != a2.Length)
            {
                throw new InvalidOperationException("Arrays are null or different lengths.");
            }

            return memcmp(a1, a2, new UIntPtr((uint)a1.Length)) == 0;
        }

        public static bool MemCmp(int[] a1, int[] a2)
        {
            if (a1 == null || a2 == null || a1.Length != a2.Length)
            {
                throw new InvalidOperationException("Arrays are null or different lengths.");
            }

            return memcmp(a1, a2, new UIntPtr((uint)(a1.Length * sizeof(int)))) == 0;
        }

        [DllImport("msvcrt.dll")]
        private static extern int memcmp(byte[] a1, byte[] a2, UIntPtr count);

        [DllImport("msvcrt.dll")]
        private static extern int memcmp(int[] a1, int[] a2, UIntPtr count);

        private const int COUNT = 10000;
    }
}

【讨论】:

  • 我测试了 PInvoke 版本,并在托管比较循环中使用ulong* 一次比较 64 位。这些数字比安全、幼稚的逐个元素循环要好得多(正如你所说,这并不奇怪)。
  • 感谢您的回答。它有助于实现我现在拥有的解决方案:对于长度
【解决方案3】:

我同意 Daniel A. White 的 cmets 的说法,它可能不会导致性能提升,而是会因为编组到非托管代码等的开销而导致性能下降。

话虽如此,你应该可以使用GCHandle.Alloc

public unsafe bool Equals(T[] x, T[] y)
{
    if (ReferenceEquals(x, y)) return true;
    if (x == null || y == null) return false;
    if (x.Length != y.Length) return false;

    GCHandle handleOfX = default(GCHandle);
    GCHandle handleOfY = default(GCHandle);
    handleOfX = GCHandle.Alloc(x, GCHandleType.Pinned);
    handleOfY = GCHandle.Alloc(y, GCHandleType.Pinned);
    try
    {
        return memcmp(handleOfX.AddrOfPinnedObject(),
                      handleOfY.AddrOfPinnedObject(),
                      x.Length * this.elementSize);
    }
    finally
    {
        if(handleOfX != default(GCHandle)) handleOfX.Free();
        if(handleOfY != default(GCHandle)) handleOfY.Free();
    }
}

【讨论】:

  • 我将对这种方法进行基准测试并发布我的发现。我担心 GCHandle 是重量级的。固定不使用它们。 (顺便说一句,我只会对大数组进行 PInvoke。我将比较不安全托管代码中的小数组,因此根本没有调用开销)。
  • 另外检查一下:techmikael.blogspot.co.uk/2009/01/… 我认为它会加快 LARGE 数组的速度(尽管我认为使用 GC 的版本与仅修复数组相比会增加很多开销 - 但是课程修复将导致不安全的代码。
  • GCHandle 的性能真的很糟糕。我没想到会那么糟糕。尽管如此,它仍然是唯一允许不专注于 T 的解决方案。
猜你喜欢
  • 2012-12-05
  • 1970-01-01
  • 1970-01-01
  • 2021-10-07
  • 2010-11-13
  • 2021-01-24
  • 1970-01-01
  • 2012-07-18
  • 2017-06-17
相关资源
最近更新 更多