65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET 中使用 VB 和 CIL 移动内存

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (42投票s)

2014年5月2日

CPOL

10分钟阅读

viewsIcon

38690

.NET 编程和性能考虑的主题和方法

引言

我在现代 .NET 时代移动内存的冒险始于我决定想编程我自己计算机的硬件。我发现 WMI 慢得无法忍受,而且我拒绝相信我是在幻觉中说它慢得无法忍受

当时我有很多事情要同时处理。必须进行大量的 p/Invoke 调用,并且需要进行大量的复杂结构处理。UnmanagedMemoryAccessor 对象对我来说并不像我希望的那样有用,所以我决定 sort of 重新发明轮子,但有目的。

我需要(想要)磁盘访问、网络访问、文件关联访问、虚拟磁盘访问、硬件树访问,我真的需要进行大量的内存操作。

但 .NET Framework 中找不到这些。我们最接近所有这些硬件和虚拟驱动器接口的东西是 COM 和 WMI,我们都知道这些东西需要一些时间来处理。我欣赏并理解拥有 WMI 这样的标准是有用的,但个人认为它的组织和执行方式对于常见任务来说有点反直觉、令人困惑且不方便。

所以,我承担了所有这些不同的接口并重新抽象了它们。我从头开始,再次编写它们。当然,这就是 .NET Framework 中总是完成的方式(至少,应该完成的方式):我研究了 Windows API 本身的基础“C”语言代码,阅读了 MSDN 文档的卷册,并将 CLI 库包装在我学到的所有东西周围。

通常,可以公平地说,如果有人想在 C# 或 VB 中复制一块内存,他们会 p/Invoke kernel32.dll 中的 RtlMoveMemory 函数或 msvcrt.dll 中的 memcpy 函数。

然而,这不是最高效的实现方式。

在我的研究中,我意识到与非托管内存交互的标准函数和提供的函数效率低下且令人困惑(尤其是在编写新抽象的 API 时,需要大量内存非常快速地在操作系统和应用程序之间移动)。

我用我的小型内存项目解决了这个问题:.NET Memory Tools

但这仅仅是我旅程的开始。

背景

Visual Basic 无法表达的三个 CIL 操作码是 cpblk 和 ldind/stind。即使是 C# 也不能本地生成 cpblk。

在我看来,这些东西是 .NET Framework 中几乎所有事物背后的秘密……或者至少是它的速度

由于 Visual Basic 无法生成某些 IL 操作码,因此我自然而然地开始了如何完成此事的探索。经过多年的思考,除了 p/Invoke 来复制我的内存(不使用有些不方便的 InteropServices.Marshal 类),我意识到我可以编写虚拟 IL 代码;我意识到性能存在巨大差异(尤其是在移动大量内存时),并且我意识到我用于提高内存访问性能的各种方法之间也存在巨大差异。

突然,图表

(从非托管内存到数组元素以及反之,以逐字节的方式测试 1,000,000,000 字节。)

Virtual Via Delegate Set Value:    00:00:05.4812442 
VB/Pure IL Property Set:           00:00:02.3712762
C#/Unsafe Property Set:            00:00:02.2942013
Virtual Via Delegate Get Value:    00:00:07.4971873
VB/Pure IL Property Get:           00:00:02.8006884
C#/Unsafe Property Get:            00:00:02.4123148
All of the Above, In One Loop:     00:00:20.5727366
p/Invoke CopyMemory Set:           00:00:24.9369384
p/Invoke CopyMemory Get:           00:00:25.2172065    

所有测试均在配备 16 GB 1333 DRAM 的 Intel Core i5 4430 Haswell 计算机上运行 Windows 8.1。

使用 RyuJIT 测试

RyuJIT 是 Microsoft 正在开发的用于未来在开发人员套件中部署的新即时 (Just-In-Time) 编译器。与使用当前生产编译器运行的测试相比,使用 RyuJIT 运行的测试平均快 15%

在动态函数中使用 IL 操作码

CIL 或通用中间语言,是一种汇编语言,所有 .NET 语言都会编译成它。当程序运行时,CIL 字节码由 JIT 或 AOT 编译器执行。

MSDN 库中有一个操作码参考,说明了它们如何表达和使用。

在 VisualBasic 中构造 MemCpy 函数相对直接

首先,您需要导入相应的命名空间

Imports System.Reflection.Emit
接下来,您需要声明您的动态委托和函数
Public Delegate Sub MemCpyFunc(dest As IntPtr, src As IntPtr, byteLen As UInteger) 
Public ReadOnly MemCpy As MemCpyFunc   

最后,您需要将此代码放入一个模块,或任何将在需要 MemCpy 之前调用的构造函数中。

总而言之,如此

Imports System.Reflection.Emit

Module Native
    Public Delegate Sub MemCpyFunc(dest As IntPtr, src As IntPtr, byteLen As UInteger)
    Public ReadOnly MemCpy As MemCpyFunc
  
    Sub New()
    ' Create a new dynamic method with the appropriate input and output parameters.

        Dim dynMtd As New DynamicMethod _
               (
                   "MemCpy",
                   GetType(Void),
                   {GetType(IntPtr), GetType(IntPtr), GetType(UInteger)}, GetType(Native)
               )

        Dim ilGen As ILGenerator = dynMtd.GetILGenerator()

        ' Load the first argument of the procedure.
        ' This will be the destination memory address (IntPtr)
        ilGen.Emit(OpCodes.Ldarg_0)
        ' Load the second argument of the procedure.
        ' This will be the source memory address (IntPtr)
        ilGen.Emit(OpCodes.Ldarg_1)
        ' Load the third argument of the procedure.
        ' This is the number of bytes to copy (UInteger)
        ilGen.Emit(OpCodes.Ldarg_2)

        ' Copy the block of memory using the Cpblk Opcode.
        ilGen.Emit(OpCodes.Cpblk)

        ' Return
        ilGen.Emit(OpCodes.Ret)

        ' Create a delegate from the emitted dynamic method.
        MemCpy = CType(dynMtd.CreateDelegate(GetType(MemCpyFunc)), MemCpyFunc)
    End Sub

End Module  

将纯 IL 整合到函数中

有时,您可能希望在 Visual Basic 或 C# 库中实现一个需要使用语言中不支持的操作码的函数。

如前所述,Visual Basic 在日常编程中最常使用的三个 IL 操作码是 cpblk、ldind 和 stind。cpblk,如上所示,将内存段从一个内存位置复制到另一个位置。另一方面,ldind 和 stind 用于从内存指针中检索可编译的变量。支持的变量包括有符号和无符号整数以及长度最多为 8 字节的浮点变量。这两个操作码可以在 C# 的“不安全代码”功能中实现,作为指针解引用。

为了在主要用 Visual Basic 编写的库中包含纯 IL 函数,我选择了一个名为IL Support 的免费 Visual Studio 插件。IL Support 增加了将纯 .il 文件编译为“部分”类或现有 Visual Basic 或 C# 代码中类的扩展的能力。为了实现这一点,该插件使用了一个可以应用于实现所谓前向引用的函数的属性,指示函数的实现另外提供。我们这样做是为了提供一个可以在编译器环境中使用的声明,使 IntelliSense 能够验证您的代码。IL 函数的实现取决于您。调试也有一点难度,因为您不能总是单步调试一个行为异常的 IL 函数。IL Support 还提供了一个带有语法高亮的简单编辑器。

首先,我们需要导入一个命名空间

Imports System.Runtime.CompilerServices 

接下来,我们在 VB 中声明函数

Namespace Memory
    
    Public Class MemoryTool

        Public Handle As IntPtr
         
        <MethodImpl(MethodImplOptions.ForwardRef)>
        Public Function GrabBytes(byteIndex As IntPtr, length As Integer) As Byte()
            Return Nothing
        End Function
    
    End Class
    
End Namespace  

如上定义的函数返回“Nothing”。这行代码在最终编译时被忽略,并且由于 MethodImp() 属性,下面的代码会被插入到它的位置。我将该行代码放在那里是为了防止 IntelliSense 抛出关于无返回值函数调用的警告。

byteIndex 参数声明为 IntPtr ,因为在 CIL 中,IntPtr 被转换为称为 native int 的类型。Native int 的大小取决于用于编译二进制的平台:32 位平台为 4 字节,64 位平台为 8 字节。这为 byteIndex 的可能值提供了自然的计算限制。

如您所见,我们为其提供了命名空间和类,以便可以演示该功能的全部内容。
接下来,我们将实现附带的 .il 文件中的实际函数

.namespace Memory
{
    .class public MemoryTool
    {
        .method public instance uint8[]
                GrabBytes(native int byteIndex, int32 length) cil managed 
        {
            .maxstack 3
            .locals init
            (
                uint8[] x
            )
            ldarg.0
            ldfld       native int Memory.MemoryTool::Handle
            ldarg.1
            add
            starg 1
            ldarg.2
            newarr      [mscorlib]System.Byte
            stloc.0
            ldloc.0
            ldc.i4.0
            ldelema     [mscorlib]System.Byte
            ldarg.1
            ldarg.2
            
            volatile.
            cpblk
            
            ldloc.0
            ret        
        }
    }
}

IL Support 与 Visual Studio 2013

在撰写本文时,当前版本的 IL Support 默认情况下无法正确安装到 Visual Studio 2013,尽管可以通过两个步骤使其正常工作。

首先,下载 .vsix 文件,将其复制到一个临时文件并赋予它一个 .zip 扩展名。打开 .zip 文件,其中将有一个名为“extension.vsixmanifest”的文件。打开该文件,找到此块

<VisualStudio Version="11.0">
    <Edition>Ultimate</Edition>
    <Edition>Premium</Edition>
    <Edition>Pro</Edition>
    <Edition>IntegratedShell</Edition>
</VisualStudio>  

直接在此块下方(在 </VisualStudio> 之后),插入以下文本块

<VisualStudio Version="12.0">
    <Edition>Ultimate</Edition>
    <Edition>Premium</Edition>
    <Edition>Pro</Edition>
    <Edition>IntegratedShell</Edition>
</VisualStudio> 

将文件保存回 .zip,并将文件恢复为其原始的“.vsix”扩展名。您现在将能够为 Visual Studio 2013 安装 IL Support。

但是,此时的问题是 IL Support 找不到 ilasm 和 ildasm,因为 IL Support 会执行一个后编译操作,将编译的项目反汇编成 IL,插入用户开发的 IL 代码,然后重新编译最终的 EXE 或 DLL。

要使程序能够实际编译,您需要将所有文件从

C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools 

to

C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin 

此方法在 Windows 8.1 上运行完美。我不知道 Windows 7 需要使用哪种方法,但我假设存在类似的解决方法。

.NET Framework 本身可能存在缺陷

我自己查看了 .NET Framework 的代码,并且对 BufferUnmanagedMemoryAccessor BitConverter 类感到有些困惑,它们都是以任何普通 .NET 程序员可能编写它们的方式编写的。当我拿到代码并意识到它们在 p/Invoke 到 MemCpy 来移动大部分内存时,我感到非常失望。

我看到的一个普遍说法是,cpblk CIL 操作码在 32 位版本的 .NET Framework 粘合层中实现不佳,以逐字节的方式移动内存。64 位版本更聪明,因为它使用 128 位寄存器进行内存复制。CIL 操作码编译成所谓的 JIT/AOT 编译器的“快车道”……处理这些操作码的预编译逻辑非常少;它们是实时运行的,就像纯字节码汇编一样。另一方面,MemCpy ,无论他们如何优化调用,仍然是 p/Invoke。

我的发现让我目瞪口呆。我只希望他们能在他们即将推出的 RyuJIT 和 Roslyn 构建中改进这种情况。但对于任何可能需要知道这一点的人来说:据我所知,他们自 .NET 2.0 以来就没有更改过 Buffer.BlockCopy 的代码。

毕竟,像 cpblk localloc 这样的东西是 .NET Framework 本身就内置的,但它们根本没有被利用,即使是对于它们的代码中最简单的任务,甚至是 mscorlib

我从未得到答案的一个问题是,“如果你自己构建了它,为什么你不使用它?”

我有点失望,不仅因为他们未能利用自己的技术,而且因为他们甚至没有觉得有必要纠正他们对一个基本但有缺陷的操作的原始实现。

更新:Microsoft 目前正在审查此情况。

总结与结论

使用纯 IL 操作码操作内存的优点怎么强调都不为过,尤其是对于使用 Visual Basic 进行编程的开发人员。Visual Basic 编程语言根本无法执行一些常见的内存操作任务,而不使用内置的 .NET Framework 类数组;有时外部函数调用可能会产生不可接受的性能下降,尤其是在进行快速内存操作时。

我认为,Microsoft 的语言团队至少应该考虑支持将 CIL 嵌入到 Visual Basic 或 C# 应用程序中。我还认为,为了让 Visual Basic 真正与 C# 平起平坐,需要有一种方法允许该语言生成 ldind 和 stind。随着 Microsoft 的开放编译器项目Roslyn 的诞生,我们可能很快就有机会探索一些实现这一目标的方法。

我非常喜欢用 Visual Basic 编程。我也喜欢在 .NET 平台上开发并利用 Microsoft 开发的许多令人兴奋的技术,包括 WPF 和 WCF。我希望看到更多的支持原生语言内存操作的功能被包含到 Visual Basic 中,但目前,这里建议的方法应该能为那些可能在实现与 C# 相当的性能方面遇到困难的 Visual Basic 程序员提供一些缓解。

特别感谢

我想特别感谢 Lucian WischikAnthony D. Green,他们是 Microsoft 的 Visual Basic 语言团队的联合负责人。他们在探索这些途径时给了我宝贵的建议,并就我的项目研究的各种主题提供了大力支持。我还可以说他们很有幽默感。

外部链接和代码

© . All rights reserved.