C# 7.2中的In关键字性能

12

我正试图测试在 C# 中添加的 "in" 关键字的性能表现(或不行)。"in" 关键字应该能够将只读引用传递给值类型,而不是先复制值再传递。

通过绕过这个复制,"in" 应该更快,但在我的测试中似乎根本没有更快。

我正在使用 BenchMarkDotNet 来对我的代码进行基准测试。代码看起来像:

public struct Input
{
    public decimal Number1 { get; set; }
    public decimal Number2 { get; set; }
}

public class InBenchmarking
{
    const int loops = 50000000;
    Input inputInstance;

    public InBenchmarking()
    {
        inputInstance = new Input
        {
        };
    }

    [Benchmark]
    public decimal DoSomethingRefLoop()
    {
        decimal result = 0M;
        for (int i = 0; i < loops; i++)
        {
            result = DoSomethingRef(ref inputInstance);
        }
        return result;
    }

    [Benchmark]
    public decimal DoSomethingInLoop()
    {
        decimal result = 0M;
        for (int i = 0; i < loops; i++)
        {
            result = DoSomethingIn(inputInstance);
        }
        return result;
    }


    [Benchmark(Baseline = true)]
    public decimal DoSomethingLoop()
    {
        decimal result = 0M;
        for (int i = 0; i < loops; i++)
        {
            result = DoSomething(inputInstance);
        }
        return result;
    }

    public decimal DoSomething(Input input)
    {
        return input.Number1;
    }

    public decimal DoSomethingIn(in Input input)
    {
        return input.Number1;
    }

    public decimal DoSomethingRef(ref Input input)
    {
        return input.Number1;
    }
}

如您所见,我包含了一个循环来使用"ref"关键字,它也是通过引用传递的,但不是只读的。这似乎更快。

此测试的结果为:

             Method |     Mean |     Error |    StdDev | Scaled | ScaledSD |
------------------- |---------:|----------:|----------:|-------:|---------:|
 DoSomethingRefLoop | 20.15 ms | 0.3967 ms | 0.6058 ms |   0.41 |     0.03 |
  DoSomethingInLoop | 48.88 ms | 0.9756 ms | 2.5529 ms |   0.98 |     0.08 |
    DoSomethingLoop | 49.84 ms | 1.0872 ms | 3.1367 ms |   1.00 |     0.00 |

使用"in"似乎并没有更快。我感觉可能有一些优化是以一种我没有预料到的方式进行的,这解释了性能差异。我已经尝试将结构体的大小增加到16个十进制字段,但在传递方式为"in"和按值传递之间并没有区别。

我该如何构建基准测试以真正看到"in"、"ref"和按值传递之间的差异?


2
如果方法调用足够小以内联,则不应有任何差别。您的基准测试无论如何都没有使用返回值,更为聪明的优化器(例如在C++编译器中找到的优化器)将会删除循环和测试代码。 - Ben Voigt
你好,BenchmarkDotNet库将使用返回值以避免被优化。因此,我从DoSomething一直使用返回值。虽然你是对的,循环可以被移除。但那样就无法解释为什么“ref”会更快了。 - MindingData
1
如果结构体很小,那么“复制它”应该会更快(或者更快)。尝试使用一个非常庞大的结构体类型(例如数百字节)。这也可以揭示任何基准测试异常或找到性能变化特征。 - user2864740
1
我相信这属于不必要的预优化范畴。 - Krythic
3
不是预先优化,只是我试图理解 in 关键字的工作原理而已 :) - MindingData
显示剩余4条评论
1个回答

12
问题在于您正在使用一个非readonly的结构体,因此编译器会在DoSomethingIn方法内创建输入参数的防御性副本。 这是因为您正在使用Number1属性的getter方法,而编译器不确定结构体状态是否会因此而改变(由于参数是作为只读引用传递的,因此这是无效的)。 如果您像这样编辑结构体:
public readonly struct Input
{
    public decimal Number1 { get; }
    public decimal Number2 { get; }
}

如果您再次运行基准测试,并使用 in 方法与 ref 方法相比,将获得相同的性能,就像您最初假设的那样。

注意: readonly struct 修饰符并非强制要求,您也可以直接公开字段来解决此问题,如下所示:

public struct Input
{
    public decimal Number1;
    public decimal Number2;
}

重点在于,如此处所述:

编译器无法知道任何成员方法是否修改了结构的状态。为确保对象不被修改,编译器会创建一个副本,并使用该副本调用成员引用。任何修改都是针对该防御性副本进行的。

编辑#2:为了进一步说明为什么需要readonly struct修饰符(因为再次强调,inref readonly相同),以下是文档中的另一段话:

[...] 其他时候,您可能希望创建一个不可变的结构。然后,您始终可以通过只读引用来传递。 这种做法消除了作为输入参数使用的结构的方法访问时发生的防御性拷贝。


1
嗯,这确实可行,但我正在努力更好地理解它。在 In 的官方 MS 文档中(https://learn.microsoft.com/en-us/dotnet/csharp/reference-semantics-with-value-types),他们有一个示例代码,应该使用 in 关键字可以更快。他们用作示例的结构体是 Point3D(https://msdn.microsoft.com/en-us/library/system.windows.media.media3d.point3d(v=vs.110).aspx),它不是只读的。 - MindingData
1
这没有意义。in 的整个重点在于“您通过引用传递参数,被调用的方法不会修改传递给它的值” 。readonly 没有必要。此外,您最后一段与 in 无关,而是针对 ref readonly returns - user585968
1
@MindingData 当然结果应该是1。这就是值类型的全部意义,无论它们是否为in。仅仅因为现在它们被传递的方式看起来像是对象引用,并不改变规则,否则小猫咪们就会到处死亡。 - user585968
1
@Sergio0694 嗯,我不认为你理解这个问题。MindingData理解。看看他的最后一条评论和Gist。那正好支持了我的观点。 - user585968
1
或者说,只需将同一变量作为“in”参数和“ref”参数传递,并查看对后者的更改是否可见于前者。 - Ben Voigt
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接