包含固定数组的托管不安全结构上的 C# 固定语句的开销是多少?

Posted

技术标签:

【中文标题】包含固定数组的托管不安全结构上的 C# 固定语句的开销是多少?【英文标题】:What is the overhead of C# fixed statement on a managed unsafe struct containing fixed arrays? 【发布时间】:2012-01-19 18:53:20 【问题描述】:

我一直在尝试确定在 C# 中对包含固定数组的托管不安全结构使用固定语句的真正成本。请注意,我指的不是非托管结构。

具体来说,是否有任何理由避免下面的“MultipleFixed”类显示的模式?简单地修复数据的成本是非零的,接近于零(== 成本类似于在进入/退出固定范围时设置和清除单个标志),还是在可能的情况下足以避免?

显然,这些类是为了帮助解释问题而设计的。这是针对 XNA 游戏中的高使用率数据结构,其中该数据的读/写性能至关重要,所以如果我需要修复数组并将其传递到任何地方,我会这样做,但如果没有区别,我'希望将 fixed() 保留在方法的本地,以帮助保持函数签名对不支持不安全代码的平台稍微更具可移植性。 (是的,它有一些额外的 grunt 代码,但不管它需要什么......)

不安全的结构 ByteArray 公共固定字节数据[1024]; 类MultipleFixed unsafe void SetValue(ref ByteArray bytes, int index, byte value) 固定(字节*数据=字节。数据) 数据[索引] = 值; unsafe bool Validate(ref ByteArray bytes, int index, byte expectedValue) 固定(字节*数据=字节。数据) 返回数据[索引] == 预期值; 无效测试(参考 ByteArray 字节) SetValue(ref bytes, 0, 1); 验证(参考字节,0, 1); 类 SingleFixed unsafe void SetValue(byte* data, int index, byte value) 数据[索引] = 值; unsafe bool Validate(byte* data, int index, byte expectedValue) 返回数据[索引] == 预期值; 不安全的无效测试(参考 ByteArray 字节) 固定(字节*数据=字节。数据) SetValue(数据, 0, 1); 验证(数据,0, 1);

另外,我查找了类似的问题,发现最接近的是this,但这个问题的不同之处在于它只涉及纯托管代码以及在该上下文中使用固定的具体成本。

感谢您提供任何信息!

【问题讨论】:

也许改变接受的答案。很明显,接受的那个是不准确的。 【参考方案1】:

这实际上是我自己提出的有趣问题。

我设法获得的结果表明性能损失的原因与“固定”语句本身略有不同。

您可以在下面看到我运行的测试和结果,但我从中得出以下观察结果:

在没有 IntPtr 的情况下,使用带有纯指针 (x*) 的“固定”的性能与在托管代码中一样好;在发布模式下,如果不经常使用固定,甚至会更好 - 这是访问多个数组值的最有效方式 在调试模式下,使用“固定”(在循环内)会对性能产生很大的负面影响,但在发布模式下,它的效果几乎与普通数组访问(方法 FixedAccess)一样好; 在引用类型参数值 (float[]) 上使用“ref”始终具有更高或相同的性能(两种模式) 在使用 IntPtr 算术 (IntPtrAccess) 时,调试模式与发布模式相比性能显着下降,但对于这两种模式,性能都比普通数组访问差 如果使用未与数组值的偏移量对齐的偏移量,则无论模式如何,性能都很糟糕(这两种模式实际上需要相同的时间)。这适用于“float”,但对“int”没有影响

多次运行测试,得出的结果略有不同但大致一致。也许我应该进行许多系列测试并取平均时间 - 但没有时间这样做:)

先测试类:

class Test 
    public static void NormalAccess (float[] array, int index) 
        array[index] = array[index] + 2;
    

    public static void NormalRefAccess (ref float[] array, int index) 
        array[index] = array[index] + 2;
    

    public static void IntPtrAccess (IntPtr arrayPtr, int index) 
        unsafe 
            var array = (float*) IntPtr.Add (arrayPtr, index << 2);
            (*array) = (*array) + 2;
        
    

    public static void IntPtrMisalignedAccess (IntPtr arrayPtr, int index) 
        unsafe 
            var array = (float*) IntPtr.Add (arrayPtr, index); // getting bits of a float
            (*array) = (*array) + 2;
        
    

    public static void FixedAccess (float[] array, int index) 
        unsafe 
            fixed (float* ptr = &array[index]) 
                (*ptr) = (*ptr) + 2;
        
    

    public unsafe static void PtrAccess (float* ptr) 
        (*ptr) = (*ptr) + 2;
    


还有测试本身:

    static int runs = 1000*1000*100;
    public static void Print (string name, Stopwatch sw) 
        Console.WriteLine ("0, items/sec = 1:N \t 2", sw.Elapsed, (runs / sw.ElapsedMilliseconds) * 1000, name);
    

    static void Main (string[] args) 
        var buffer = new float[1024*1024*100];
        var len = buffer.Length;

        var sw = new Stopwatch();
        for (int i = 0; i < 1000; i++)  
            Test.FixedAccess (buffer, 55);
            Test.NormalAccess (buffer, 66);
        

        Console.WriteLine ("Starting 0:N0 items", runs);


        sw.Restart ();
        for (int i = 0; i < runs; i++) 
            Test.NormalAccess (buffer, i % len);
        sw.Stop ();

        Print ("Normal access", sw);

        sw.Restart ();
        for (int i = 0; i < runs; i++) 
            Test.NormalRefAccess (ref buffer, i % len);
        sw.Stop ();

        Print ("Normal Ref access", sw);

        sw.Restart ();
        unsafe 
            fixed (float* ptr = &buffer[0])
                for (int i = 0; i < runs; i++)  
                    Test.IntPtrAccess ((IntPtr) ptr, i % len);
                
        
        sw.Stop ();

        Print ("IntPtr access (fixed outside loop)", sw);

        sw.Restart ();
        unsafe 
            fixed (float* ptr = &buffer[0])
                for (int i = 0; i < runs; i++)  
                    Test.IntPtrMisalignedAccess ((IntPtr) ptr, i % len);
                
        
        sw.Stop ();

        Print ("IntPtr Misaligned access (fixed outside loop)", sw);

        sw.Restart ();
        for (int i = 0; i < runs; i++) 
            Test.FixedAccess (buffer, i % len);
        sw.Stop ();

        Print ("Fixed access (fixed inside loop)", sw);

        sw.Restart ();
        unsafe 
            fixed (float* ptr = &buffer[0])  
                for (int i = 0; i < runs; i++)  
                    Test.PtrAccess (ptr + (i % len));
                
            
        
        sw.Stop ();

        Print ("float* access (fixed outside loop)", sw);

        sw.Restart ();
        unsafe 
            for (int i = 0; i < runs; i++)  
                fixed (float* ptr = &buffer[i % len])  
                    Test.PtrAccess (ptr);
                
            
        
        sw.Stop ();

        Print ("float* access (fixed in loop)", sw);

最后是结果:

调试模式

Starting 100,000,000 items
00:00:01.0373583, items/sec = 96,432,000.00      Normal access
00:00:00.8582307, items/sec = 116,550,000.00     Normal Ref access
00:00:01.8822085, items/sec = 53,134,000.00      IntPtr access (fixed outside loop)
00:00:10.5356369, items/sec = 9,492,000.00       IntPtr Misaligned access (fixed outside loop)
00:00:01.6860701, items/sec = 59,311,000.00      Fixed access (fixed inside loop)
00:00:00.7577868, items/sec = 132,100,000.00     float* access (fixed outside loop)
00:00:01.0387792, items/sec = 96,339,000.00      float* access (fixed in loop)

发布模式

Starting 100,000,000 items
00:00:00.7454832, items/sec = 134,228,000.00     Normal access
00:00:00.6619090, items/sec = 151,285,000.00     Normal Ref access
00:00:00.9859089, items/sec = 101,522,000.00     IntPtr access (fixed outside loop)
00:00:10.1289018, items/sec = 9,873,000.00       IntPtr Misaligned access (fixed outside loop)
00:00:00.7899355, items/sec = 126,742,000.00     Fixed access (fixed inside loop)
00:00:00.5718507, items/sec = 175,131,000.00     float* access (fixed outside loop)
00:00:00.6842333, items/sec = 146,198,000.00     float* access (fixed in loop)

【讨论】:

【参考方案2】:

根据经验,在最好的情况下,开销似乎在 32 位 JIT 上约为 270%,在 64 位上约为 200%(并且开销会随着您“调用”fixed 的次数越多而变得更糟)。所以如果性能真的很关键,我会尽量减少你的 fixed 块。

抱歉,我对固定/不安全代码不够熟悉,不知道为什么会这样


详情

我还添加了一些 TestMore 方法,它们调用您的两个测试方法 10 次而不是 2 提供一个更真实的场景,即在您的 fixed 结构上调用多个方法。

我使用的代码:

class Program

    static void Main(string[] args)
    
        var someData = new ByteArray();
        int iterations = 1000000000;
        var multiple = new MultipleFixed();
        var single = new SingleFixed();

        // Warmup.
        for (int i = 0; i < 100; i++)
        
            multiple.Test(ref someData);
            single.Test(ref someData);
            multiple.TestMore(ref someData);
            single.TestMore(ref someData);
        

        // Environment.
        if (Debugger.IsAttached)
            Console.WriteLine("Debugger is attached!!!!!!!!!! This run is invalid!");
        Console.WriteLine("CLR Version: " + Environment.Version);
        Console.WriteLine("Pointer size: 0 bytes", IntPtr.Size);
        Console.WriteLine("Iterations: " + iterations);

        Console.Write("Starting run for Single... ");
        var sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        
            single.Test(ref someData);
        
        sw.Stop();
        Console.WriteLine("Completed in 0:N3ms - 1:N2/sec", sw.Elapsed.TotalMilliseconds, iterations / sw.Elapsed.TotalSeconds);

        Console.Write("Starting run for More Single... ");
        sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        
            single.Test(ref someData);
        
        sw.Stop();
        Console.WriteLine("Completed in 0:N3ms - 1:N2/sec", sw.Elapsed.TotalMilliseconds, iterations / sw.Elapsed.TotalSeconds);


        Console.Write("Starting run for Multiple... ");
        sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        
            multiple.Test(ref someData);
        
        sw.Stop();
        Console.WriteLine("Completed in 0:N3ms - 1:N2/sec", sw.Elapsed.TotalMilliseconds, iterations / sw.Elapsed.TotalSeconds);

        Console.Write("Starting run for More Multiple... ");
        sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        
            multiple.TestMore(ref someData);
        
        sw.Stop();
        Console.WriteLine("Completed in 0:N3ms - 1:N2/sec", sw.Elapsed.TotalMilliseconds, iterations / sw.Elapsed.TotalSeconds);


        Console.ReadLine();
    


unsafe struct ByteArray

    public fixed byte Data[1024];


class MultipleFixed

    unsafe void SetValue(ref ByteArray bytes, int index, byte value)
    
        fixed (byte* data = bytes.Data)
        
            data[index] = value;
        
    

    unsafe bool Validate(ref ByteArray bytes, int index, byte expectedValue)
    
        fixed (byte* data = bytes.Data)
        
            return data[index] == expectedValue;
        
    

    public void Test(ref ByteArray bytes)
    
        SetValue(ref bytes, 0, 1);
        Validate(ref bytes, 0, 1);
    
    public void TestMore(ref ByteArray bytes)
    
        SetValue(ref bytes, 0, 1);
        Validate(ref bytes, 0, 1);
        SetValue(ref bytes, 0, 2);
        Validate(ref bytes, 0, 2);
        SetValue(ref bytes, 0, 3);
        Validate(ref bytes, 0, 3);
        SetValue(ref bytes, 0, 4);
        Validate(ref bytes, 0, 4);
        SetValue(ref bytes, 0, 5);
        Validate(ref bytes, 0, 5);
    


class SingleFixed

    unsafe void SetValue(byte* data, int index, byte value)
    
        data[index] = value;
    

    unsafe bool Validate(byte* data, int index, byte expectedValue)
    
        return data[index] == expectedValue;
    

    public unsafe void Test(ref ByteArray bytes)
    
        fixed (byte* data = bytes.Data)
        
            SetValue(data, 0, 1);
            Validate(data, 0, 1);
        
    
    public unsafe void TestMore(ref ByteArray bytes)
    
        fixed (byte* data = bytes.Data)
        
            SetValue(data, 0, 1);
            Validate(data, 0, 1);
            SetValue(data, 0, 2);
            Validate(data, 0, 2);
            SetValue(data, 0, 3);
            Validate(data, 0, 3);
            SetValue(data, 0, 4);
            Validate(data, 0, 4);
            SetValue(data, 0, 5);
            Validate(data, 0, 5);
        
    

.NET 4.0 32 位 JIT 中的结果:

CLR Version: 4.0.30319.239
Pointer size: 4 bytes
Iterations: 1000000000
Starting run for Single... Completed in 2,092.350ms - 477,931,580.94/sec
Starting run for More Single... Completed in 2,236.767ms - 447,073,934.63/sec
Starting run for Multiple... Completed in 5,775.922ms - 173,132,528.92/sec
Starting run for More Multiple... Completed in 26,637.862ms - 37,540,550.36/sec

在 .NET 4.0 中,64 位 JIT:

CLR Version: 4.0.30319.239
Pointer size: 8 bytes
Iterations: 1000000000
Starting run for Single... Completed in 2,907.946ms - 343,885,316.72/sec
Starting run for More Single... Completed in 2,904.903ms - 344,245,585.63/sec
Starting run for Multiple... Completed in 5,754.893ms - 173,765,185.93/sec
Starting run for More Multiple... Completed in 18,679.593ms - 53,534,358.13/sec

【讨论】:

谢谢 - 好信息!我仍然想知道开销的根本原因是什么,但获得良好的性能是主要目标。 是的,我将听从 Skeet、Lippart 或 Gravel 的“为什么”。但是,如果您调整结构的大小,它可能会告诉您运行时是否正在为每个fixed 复制结构。我的猜测是固定操作复制了整个结构。 (另见:dotnetperls.com/fixed-buffer) 这个测试不准确。您以完全不合理的方式使用固定。正确的用法是修复一次,多次写入,取消修复。

以上是关于包含固定数组的托管不安全结构上的 C# 固定语句的开销是多少?的主要内容,如果未能解决你的问题,请参考以下文章

无法将固定大小的字节数组从结构复制到 C# 结构中的另一个数组

为啥固定大小的缓冲区只能是原始类型?

如何在 C# 中将固定字节/字符 [100] 转换为托管字符 []?

C# 不安全/固定代码

对于写入固定大小数组的不同部分的并行线程,是不是存在线程安全的 Java 数据结构?

u3d常用数据结构大总结