【问题标题】:What should I pin when working on arrays?在阵列上工作时我应该固定什么?
【发布时间】:2014-06-09 14:56:26
【问题描述】:

我正在尝试编写一个DynamicMethod 来包装cpblk IL 操作码。我需要在 x64 平台上复制字节数组块,这应该是最快的方法。 Array.CopyBuffer.BlockCopy 都可以,但我想探索所有选项。

我的目标是将托管内存从一个字节数组复制到一个新的托管字节数组。我关心的是我如何知道如何正确“固定”内存位置。我不希望垃圾收集器移动数组并破坏一切。到目前为止它有效,但我不确定如何测试这是否是 GC 安全的。

// copying 'count' bytes from offset 'index' in 'source' to offset 0 in 'target'
// i.e. void _copy(byte[] source, int index, int count, byte[] target)

static Action<byte[], int, int, byte[]> Init()
{
    var dmethod = new DynamicMethod("copy", typeof(void), new[] { typeof(object),typeof(byte[]), typeof(int), typeof(int),typeof(byte[]) },typeof(object), true);
    var il = dmethod.GetILGenerator();

    il.DeclareLocal(typeof(byte).MakeByRefType(), true);
    il.DeclareLocal(typeof(byte).MakeByRefType(), true);
    // pin the source
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Ldarg_2);
    il.Emit(OpCodes.Ldelema, typeof(byte));
    il.Emit(OpCodes.Stloc_0);
    // pin the target
    il.Emit(OpCodes.Ldarg_S,(byte)4);
    il.Emit(OpCodes.Ldc_I4_0);
    il.Emit(OpCodes.Ldelema, typeof(byte));
    il.Emit(OpCodes.Stloc_1);

    il.Emit(OpCodes.Ldloc_1);
    il.Emit(OpCodes.Ldloc_0);
    // load the length
    il.Emit(OpCodes.Ldarg_3);
    // perform the memcpy
    il.Emit(OpCodes.Unaligned,(byte)1);
    il.Emit(OpCodes.Cpblk);

    il.Emit(OpCodes.Ret);
    return dmethod.CreateDelegate(typeof(Action<byte[], int, int, byte[]>)) as Action<byte[], int, int, byte[]>;
}

【问题讨论】:

  • 使用现有的方法,它们很可能与您尝试做的非常相似,并且具有能够“作弊”的优势,即使用不暴露于内部运行的代码的系统功能运行。通过尝试以自己的方式解决这个问题,您目前正在浪费时间/金钱而没有保证收益(除非它是一个研究项目,在这种情况下一定要去做)。
  • 它们相似,但 il 操作码对大字节数组副本执行得更快。 (对于少于 10 个元素,它的性能非常差, Array.Copy 在这个领域似乎非常好)。最初,我引用了一个 C++/CLI dll 依赖项,我试图删除它也需要使用“不安全”编译选项。我试图将所有这些封装在一种动态方法中以避免这种烦恼。另一个优点是 IL 字节码不需要我使用原语。在这种情况下,我使用的是字节,但我也希望能够快速复制其他结构。
  • Buffer.BlockCopy 对于字节复制实际上比 array.copy 慢。它确实适用于结构类型,但仅适用于原语。例如,尝试用它复制 DateTimes,它就会爆炸。
  • OpCodes.Cpblkextremely slow on x86 though,视情况而定。似乎没有一般最好的算法,但其他算法似乎更稳定。不过,如果在您的用例中性能提升实际上很重要,您可以基于架构进行分支。
  • 这不是我关心的问题。这适用于 x64 位环境中的服务器代码。

标签: clr cil reflection.emit


【解决方案1】:

我相信您对固定局部变量的使用是正确的。

【讨论】:

    【解决方案2】:

    void cpblk(ref T src, ref T dst, int c_elem)

    使用 cpblk IL 指令将 T 类型的 c_elem 元素从 src 复制到 dst。元素类型T 必须描述一个非托管ValueType(或原语); cpblk 无法在任何嵌套级别复制包含 GC 对象引用的内存。请注意,c_elem 表示元素的数量,而不是字节数。使用 C#7.NET 4.7 进行测试。请参阅下面的用法示例。

    public static class IL<T>
    {
        public delegate void _cpblk_del(ref T src, ref T dst, int c_elem);
        public static readonly _cpblk_del cpblk;
    
        static IL()
        {
            var dm = new DynamicMethod("cpblk+" + typeof(T).FullName,
                typeof(void),
                new[] { typeof(T).MakeByRefType(), typeof(T).MakeByRefType(), typeof(int) },
                typeof(T),
                true);
    
            var il = dm.GetILGenerator();
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_2);
    
            int cb = Marshal.SizeOf<T>();
            if (cb > 1)
            {
                il.Emit(OpCodes.Ldc_I4, cb);
                il.Emit(OpCodes.Mul);
            }
    
            byte align;
            if ((cb & (align = 1)) != 0 ||
                (cb & (align = 2)) != 0 ||
                (cb & (align = 4)) != 0)
                il.Emit(OpCodes.Unaligned, align);
    
            il.Emit(OpCodes.Cpblk);
            il.Emit(OpCodes.Ret);
            cpblk = (_cpblk_del)dm.CreateDelegate(typeof(_cpblk_del));
        }
    }
    

    请注意,此代码假定元素是字节压缩的(即各个元素之间没有填充)并且根据它们的大小对齐。具体来说,源地址和目标地址应能被1 &lt;&lt; floor(log₂(sizeof(T) &amp; 0xF)) 整除。换句话说,如果sizeof(T) % 8 不为零,则发出OpCodes.Unaligned 前缀,指定余数在{1中的最高除数>、24}。对于 8 字节对齐,不需要前缀。

    例如,一个 11 字节的结构需要对齐前缀 1,因为即使范围中的第一个元素恰好是四对齐的,字节打包意味着相邻的元素不会.通常情况下,CLR 都是这样排列数组的,你不必担心这些问题。

    用法:

    var src = new[] { 1, 2, 3, 4, 5, 6 };
    var dst = new int[6];
    
    IL<int>.cpblk(ref src[2], ref dst[3], 2);      // dst => { 0, 0, 0, 3, 4, 0 }
    

    自动类型推断(可选):

    对于自动类型推断,您还可以包含以下类:

    public static class IL
    {
        public static void cpblk<T>(ref T src, ref T dst, int c_elem) 
            => IL<T>.cpblk(ref src, ref dst, c_elem);
    }
    

    有了这个,你不需要指定类型参数,前面的例子就变得简单了:

    IL.cpblk(ref src[2], ref dst[3], 2);
    

    【讨论】:

    • 这是一个很好的答案,而且速度很快。但这不也是System.Buffers.BlockCopy 在内部工作的方式吗?
    【解决方案3】:

    你不需要在这个方法中固定任何东西,如果你想固定然后在输入这个方法之前固定你的数组。您不需要固定任何指针,因为除非您重新启动程序,否则元素的地址总是相同的,您甚至可以毫无问题地将其存入 intptr 类型。

    .maxstack 3
    ldarg.0
    ldarg.1
    ldelema int8
    
    ldarg.2
    ldarg.3
    ldelema int8
    
    ldarg.s 4
    cpblk
    
    ret
    

    【讨论】:

    • 由于 GC 收集和压缩,堆上任何东西的地址都可以并且将一直变化,这是 .NET 的基础。这个答案中的信息是不正确的。由于 OP 要求 cpblk,这是一条托管指令,因此不需要固定,CLR 将负责更改指针地址。
    猜你喜欢
    • 1970-01-01
    • 2016-09-11
    • 2014-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    • 2023-03-16
    相关资源
    最近更新 更多