我通常使用具有零成本抽象概念(例如C和Rust)的语言进行编程.
目前,我正在使用C#语言的项目中工作.所以我想知道我是否可以安全地创建抽象和更高级别的代码而又不影响性能.
这在C#中还是可能的?对于性能至关重要的代码,我应该做尽可能低级的代码?
就像我在代码中遇到的一个示例(不要太关注这个示例,我的问题是更高层次的)一样,我需要一个可以返回多个值的函数,为此,我的第一种方法是使用元组,所以像这样:
public (int, int, float) Function();
或将该元组抽象为一个结构:
public struct Abstraction { int value1; int value2; float value3; };
public Abstraction Function();
我所期望的是,编译器会优化掉Tuple或Abstraction结构,而直接使用原始值.但是我发现,使用out参数编写代码可以提高性能:
public void Function(out int value1, out int value2, out float value3);
我猜测原因是因为在out函数中,没有创建Tuple或Abstraction结构.
out函数版本的问题在于,我真的很讨厌将参数用作返回值,因为这似乎更像是一种语言限制.
因此,最后我不确定我是否使用的配置不正确,因此JIT可以使用零成本抽象,或者这在C#中根本不可能或无法保证.
解决方法:
首先,我认为说语言“具有零成本的抽象”是没有道理的.考虑功能的抽象.是零成本吗?一般来说,只有内联它才是零成本.尽管C编译器在内联函数方面确实非常擅长,但它们并未内联所有函数,因此C语言中的函数严格来说并不是零成本的抽象.但是这种差异在实践中几乎没有多大关系,这就是为什么您通常可以将一个函数视为零成本的原因.
现在,现代C和Rust的设计和实现方式使它们尽可能使抽象零成本.这在C#中有所不同吗?的种类. C#在设计时并未特别关注零成本抽象(例如,在C#中调用lambda总是涉及有效的虚拟调用;在C中调用lambda则不涉及,这使其更容易实现零成本).而且,JIT编译器通常不能花很多时间在诸如内联的优化上,因此它们生成的抽象代码比C编译器差. (尽管从.Net Core 2.1 introduced a tiered JIT开始,这种情况将来可能会改变,这意味着它有更多的时间进行优化.)
另一方面,对JIT编译器进行了调整,使其可以在实际代码中而不是在微基准测试中正常工作(我认为这是您得出的结论:返回结构的性能较差).
在我的微基准测试中,使用结构确实具有较差的性能,但这是因为JIT决定不内联该版本的Function,这不是因为创建结构的成本或类似原因.如果我通过使用[MethodImpl(MethodImplOptions.AggressiveInlining)]修复了该问题,则两个版本均会达到相同的性能.
因此,在C#中,返回结构可以是零成本的抽象.尽管确实如此,与C相比,C#发生这种情况的可能性较小.
如果您想知道在输出参数和返回结构之间切换的实际效果,建议您编写一个更实际的基准测试,而不是微基准测试,然后看看结果是什么. (假设您使用了微基准测试,这没错.)