将 ref T 值固定为 void* 的 IL 方法(从并行代码处理 Span<T>)

Posted

技术标签:

【中文标题】将 ref T 值固定为 void* 的 IL 方法(从并行代码处理 Span<T>)【英文标题】:IL method to pin a ref T value as void* (to work on Span<T> from parallel code) 【发布时间】:2018-06-14 04:14:07 【问题描述】:

在实际问题之前,一个小小的免责声明:

这是this one 的相关/后续问题,但因为这里我在谈论一个更普遍的问题(如何固定ref T 变量以便能够对其执行指针操作),我我提出了一个可能的(可能是错误的)解决方案,我提出了一个单独的问题。

所以,问题是:给定一个 ref T 变量(假设它是数组的第一项),我如何固定它,以便 GC 在处理底层数组时不会引起问题不安全的指针?

我不确定这在 C# 中是否可行(但我希望我错了),但是查看 IL 代码以找到一种仅修复 ref int 变量的方法,我试图想出一个变体致力于泛型类型。这是我的想法:

假设我在“MyTestClass”类中有这个delegate

public unsafe delegate void UnsafeAction(void* p);

然后,在 IL:

.method public hidebysig static void
    Foo<valuetype .ctor (class [netstandard]System.ValueType) T>(
        !!0/*T*/& r, 
        class MyTestClass/UnsafeAction action
    ) cil managed

    .maxstack 2
    .locals init (
      [0] void* p,
      [1] !!0/*T*/& pinned V_1
    )

    // Load the ref T argument into the pinned variable (as if fixed were used)
    IL_0000: nop          
    IL_0001: ldarg.0      // r
    IL_0002: stloc.1      // V_1

    // Cast the T* pointer to void*
    IL_0003: ldloc.1      // V_1
    IL_0004: conv.u       
    IL_0005: stloc.0      // p

    // Invoke the action passing the p pointer
    IL_0006: ldarg.1      // action
    IL_0007: ldloc.0      // p
    IL_0008: callvirt     instance void MyTestClass/UnsafeAction::Invoke(void*)
    IL_000d: nop          

    // Set the pinned variable V_1 to NULL
    IL_000e: ldc.i4.0     
    IL_000f: conv.u       
    IL_0010: stloc.1      // V_1
    IL_0011: ret

我们的想法是能够像这样使用这种方法:

public static void Test<T>(this Span<T> span, Func<T> provider)

    void Func(void* p)
    
        // Do stuff with p, possibly in parallel
        // eg. Unsafe.Write(p, provider());
    
    Foo(ref span.DangerousGetPinnableReference(), Func);

这样的东西会起作用吗?如果是这样,将这种用 IL 编写的方法包含到现有 .NET Standard 2.0 项目中的最佳方法是什么?

谢谢!

额外问题:我已经看到 CoreFX 存储库对具有 IL 类的项目使用“.ilproj”文件,但 VS 实际上并不支持它们。这是一些扩展还是他们使用自己的自定义脚本来支持该项目格式?我知道有一个 ILProj extension 可用,但它既不是官方的,也不兼容 VS2017。

编辑:我可能刚刚知道如何解决这个问题,考虑一下:

public static void Foo<T>(ref T value)

    fixed (void* p = &Unsafe.As<T, byte>(ref value))
    
        // Shouldn't the ref T be correctly fixed here, since
        // the "dummy" byte ref had the same address?
    

【问题讨论】:

我已经看到 CoreFX 存储库使用“.ilproj”文件来处理具有 IL 类的项目,但 VS 实际上并不支持它们。那是一些扩展还是他们使用自己的自定义脚本来支持该项目格式?如果你用谷歌搜索ilproj,前三个链接会帮助你...... @ta.speot.is 是的,我这样做并看到了这些结果,但这些扩展似乎不是官方的(即不是来自 MS),第一个的描述说的是那些项目不能从 C# 项目中引用,您必须构建它们并链接 dll 本身。我希望有一个更简单的解决方案。 【参考方案1】:

是的,将引用存储在固定本地就足够了。在方法执行过程中,引用的内存不会被移动,因此指针将保持有效。

您也可以从您的方法中删除nops(分析在Debug下编译的代码的结果),之后将变量设置为零也是不必要的,因为该方法在它之后退出。

至于如何在 C# 项目中分发 CIL 代码,我会使用 DynamicMethod,但我不知道 .NET Standard 中是否可用。如果没有,将 IL 编译为 .dll.netmodule 并引用它或自动将其链接到主项目并不是什么大问题。

【讨论】:

谢谢!是的,我留下了那些nops,因为我不确定它们是用于对齐还是只是调试模式的剩余部分,很高兴听到我可以删除它们。不幸的是,DynamicMethod 在 .NET Standard 2.0 上不存在,我将尝试弄清楚如何从 .il 文件中正确获取 dll。另外,看看这个问题和那个代码,你认为这是正确的方法和唯一可能的解决方案,还是可能有另一种更简单的方法来实现这一点? @Sergio0694 ilasm /dll 应该可以工作,如果你删除了.assembly 部分,它也会创建一个.netmodule。我不经常使用Span&lt;T&gt;,所以我不知道是否有更好的解决方案来解决您的根本问题。但是,您创建的方法与我在库中使用的方法相似 (here)。唯一的区别是我将转换为指针与固定分开。 感谢您的提醒,该仓库看起来很棒!我没有看到 NuGet 链接,所以如果您还没有看到,那么您绝对应该考虑在那里发布您的库! 谢谢!已经有人建议我在 NuGet 上发布它,我很乐意,但我想在此之前将库拆分成更小的连贯包。 这是个好主意。另外,我想我刚刚对如何使用现有 API 解决这个问题有了一个想法,你介意看看我的解决方案***.com/a/48105559/3813976 和这个问题底部的编辑,让我知道你怎么看?

以上是关于将 ref T 值固定为 void* 的 IL 方法(从并行代码处理 Span<T>)的主要内容,如果未能解决你的问题,请参考以下文章

vue3表单对象固定写死的值获取不到

System.InvalidProgramException:移动第 3 方 .NET 组件时 IL 代码无效(Dundas Gauge for .NET)

IL指令详细

IL指令集

IL指令详细表

IL指令详细