ReadOnlySpan<T> 参数是不是应该使用“in”修饰符?

Posted

技术标签:

【中文标题】ReadOnlySpan<T> 参数是不是应该使用“in”修饰符?【英文标题】:Should ReadOnlySpan<T> parameters use the "in" modifier?ReadOnlySpan<T> 参数是否应该使用“in”修饰符? 【发布时间】:2018-08-24 03:55:38 【问题描述】:

C# 7.2 引入了reference semantics with value-types,除此之外,微软还开发了Span&lt;T&gt; and ReadOnlySpan&lt;T&gt; 等类型,以潜在地提高需要在连续内存区域上执行操作的应用的性能。

根据文档,一种可能提高性能的方法是通过向这些类型的参数添加 in 修饰符来通过引用传递不可变结构:

void PerformOperation(in SomeReadOnlyStruct value)


我想知道我是否应该对ReadOnlySpan&lt;T&gt; 之类的类型执行此操作。我是否应该像这样声明接受只读范围的方法:

void PerformOperation<T>(in ReadOnlySpan<T> value)


或者简单地说:

void PerformOperation<T>(ReadOnlySpan<T> value)


前者会比后者提供任何性能优势吗?我找不到任何明确建议任何方向的文档,但我确实找到了this example,他们在其中演示了一种接受ReadOnlySpan 的方法,并且没有使用in 修饰符。

【问题讨论】:

我不认为这有什么区别,但是设置一些基准并让我们知道 @MarcGravell 有道理,谢谢。 我看看能不能设置一些基准。 非常不同的概念。 in 关键字提供了一种可能的优化,可以通过引用传递大型结构类型,但无需支付将其复制回调用者的成本。 ReadOnlySpan 不大,使用in 会使代码变慢。利用in 是一项艰巨的工作,您首先需要一个很好的理由来尝试它,只有分析器才能告诉您是否值得一试。不常见。 【参考方案1】:

马克的回答似乎恰到好处。我发布这篇文章只是为了补充他自己的答案,并用一些可以确认他所说的话的基准。

我设置了以下基准类:

public class SpanBenchmarks

    private const int Iterations = 100_000;

    private byte[] _data;
    private LargeStruct _control;

    [GlobalSetup]
    public void GlobalSetup()
    
        _data = new byte[1000];
        new Random().NextBytes(_data);

        _control = new LargeStruct(_data[0], _data[1], _data[2], _data[3], _data[4], _data[5]);
    

    [Benchmark]
    public void PassSpanByValue()
    
        for (int i = 0; i < Iterations; i++) AcceptSpanByValue(_data);
    

    [Benchmark]
    public void PassSpanByRef()
    
        for (int i = 0; i < Iterations; i++) AcceptSpanByRef(_data);
    

    [Benchmark]
    public void PassLargeStructByValue()
    
        for (int i = 0; i < Iterations; i++) AcceptLargeStructByValue(_control);
    

    [Benchmark]
    public void PassLargeStructByRef()
    
        for (int i = 0; i < Iterations; i++) AcceptLargeStructByRef(_control);
    

    private int AcceptSpanByValue(ReadOnlySpan<byte> span) => span.Length;
    private int AcceptSpanByRef(in ReadOnlySpan<byte> span) => span.Length;
    private decimal AcceptLargeStructByValue(LargeStruct largeStruct) => largeStruct.A;
    private decimal AcceptLargeStructByRef(in LargeStruct largeStruct) => largeStruct.A;

    private readonly struct LargeStruct
    
        public LargeStruct(decimal a, decimal b, decimal c, decimal d, decimal e, decimal f)
        
            A = a;
            B = b;
            C = c;
            D = d;
            E = e;
            F = f;
        

        public decimal A  get; 
        public decimal B  get; 
        public decimal C  get; 
        public decimal D  get; 
        public decimal E  get; 
        public decimal F  get; 
    

我用这个重复了同样的基准测试工作 3 次,每次都得到相似的结果:

BenchmarkDotNet=v0.10.13, OS=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.248)
Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical cores and 4 physical cores

Frequency=3507500 Hz, Resolution=285.1033 ns, Timer=TSC
.NET Core SDK=2.1.300-preview2-008354
  [Host]     : .NET Core 2.0.6 (CoreCLR 4.6.26212.01, CoreFX 4.6.26212.01), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.26212.01, CoreFX 4.6.26212.01), 64bit RyuJIT


                 Method |      Mean |     Error |    StdDev |
----------------------- |----------:|----------:|----------:|
        PassSpanByValue | 641.71 us | 0.1758 us | 0.1644 us |
          PassSpanByRef | 642.62 us | 0.1524 us | 0.1190 us |
 PassLargeStructByValue | 390.78 us | 0.2633 us | 0.2463 us |
   PassLargeStructByRef |  35.33 us | 0.3446 us | 0.3055 us |

使用大型结构作为控件,我确认通过引用而不是值传递它们时具有显着的性能优势。但是,通过引用或值传递 Span&lt;T&gt; 之间没有显着的性能差异。


2019 年 9 月更新

出于好奇,我再次使用 .NET Core 2.2 运行了相同的基准测试。自上次以来似乎已经引入了一些巧妙的优化,以减少将数组隐式转换为 Span&lt;T&gt; 的开销:

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.984 (1803/April2018Update/Redstone4)
Intel Core i7-4700HQ CPU 2.40GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=2.2.106
  [Host]     : .NET Core 2.2.4 (CoreCLR 4.6.27521.02, CoreFX 4.6.27521.01), 64bit RyuJIT
  DefaultJob : .NET Core 2.2.4 (CoreCLR 4.6.27521.02, CoreFX 4.6.27521.01), 64bit RyuJIT


|                 Method |      Mean |     Error |    StdDev |
|----------------------- |----------:|----------:|----------:|
|        PassSpanByValue |  39.78 us | 0.1873 us | 0.1660 us |
|          PassSpanByRef |  41.21 us | 0.2618 us | 0.2186 us |
| PassLargeStructByValue | 475.41 us | 1.3104 us | 1.0943 us |
|   PassLargeStructByRef |  39.75 us | 0.1001 us | 0.0937 us |

【讨论】:

【参考方案2】:

这里的一个关键因素是尺寸; Span&lt;T&gt; / ReadOnlySpan&lt;T&gt; 故意非常小,因此跨度和对跨度的引用之间的差异很小。 in 的一个关键用法是 larger 只读结构,以避免显着的堆栈复制;请注意,有一个权衡:in 实际上是ref,因此您为所有访问添加了额外的间接层,除非 JIT 看到您在做什么并使用一些巫术。当然:如果类型将自己声明为readonly,则堆栈副本会自动添加调用之前以保留语义。

【讨论】:

我的印象是ref readonly structs,例如SpanReadOnlySpan 已经通过引用传递(它们甚至命名为“类引用类型”),这似乎不是真的? @Evk ref in ref struct 仅表示“仅允许在堆栈上”(因此:允许将类引用类型和托管指针作为字段保存);这并不意味着它是通过引用传递的;一些最有用的ref struct 类型实际上是故意可变的。 readonly 修饰符强制执行某些语义,允许在没有意外更改的情况下可靠地使用它,这允许 in 在保证(至少在语言级别,而不是在 IL level) 值不会被修改。 @Evk 关键是它允许你拥有(例如)一个跨度字段,否则这是不允许的 - 或任何其他类似 ref 的类型(同样只存在于在堆栈上) 是的,我想我理解这个意图,但由于某种原因,我也有印象他们也被 ref 传递。也许定义中的“参考”让我感到困惑。他们可以使用一些更有意义的名称,例如“stackonly”之类的。 @Evk 添加关键字 - 甚至是上下文关键字 - 比重新利用和重载现有保留关键字更难:)

以上是关于ReadOnlySpan<T> 参数是不是应该使用“in”修饰符?的主要内容,如果未能解决你的问题,请参考以下文章

.net core多线程新增数据,使用线程安全类型ReadOnlySpan<T>

.net core多线程新增数据,使用线程安全类型ReadOnlySpan<T>

C# 7.1 和 7.2 Span 和 ReadOnlySpan

T::iterator 出错,其中模板参数 T 可能是 vector<int> 或 list<int>

为啥 F# (FSharpOption<T>) 中的默认参数是引用类型?

接受 Result<T, E> 作为函数参数是惯用的 Rust 吗?