我通常使用的语言是零成本抽象概念,比如C++和Rust。
目前,我正在一个使用C#语言的项目中工作。因此,我想知道我是否能够在不影响性能的情况下安全地创建抽象和高级代码。
这在C#中是可能的,还是对于性能关键的代码,我应该尽可能地做低级别的代码?
正如我在代码中遇到的一个例子(不要过多地关注这个示例,我的问题是更高的级别),我需要一个返回多个值的函数,为此,我的第一种方法是使用tuple,所以如下所示:
public (int, int, float) Function();
或者将这个元组抽象成一个结构:
public struct Abstraction { int value1; int value2; float value3; };
public Abstraction Function();
我所期望的是编译器将优化Tuple
或Abstraction struct
,直接使用原语值。但是,我发现使用out
参数编写代码会提高性能:
public void Function(out int value1, out int value2, out float value3);
我猜原因是因为在out
函数中,没有创建Tuple
或Abstraction struct
。
out
函数版本的问题是,我真的不喜欢使用参数作为返回值,因为它更像是对语言限制的攻击。
因此,最后,我不确定我是否只是没有使用正确的配置,这样JIT就可以使用零成本抽象,或者这在C#中是不可能的,或者是没有保证的。
发布于 2018-09-29 21:43:37
首先,我认为说语言“有零成本的抽象”是没有意义的。考虑函数的抽象。是零成本吗?一般来说,只有当它是内联的时候,它才是零成本。虽然C++编译器对内联函数很在行,但它们并不是所有函数都内联,所以C++中的函数严格地说不是零成本的抽象。但这种差异在实践中很少起作用,这就是为什么你通常可以认为一个函数是零成本的。
现在,现代C++和Rust的设计和实现方式使抽象尽可能多地成为零成本。这在C#中是不同的吗?有点。C#的设计并没有如此注重于零成本抽象(例如,在C#中调用lambda总是涉及到什么实际上是虚拟调用;在C++中调用lambda则不需要,这使得它更容易使其为零成本)。而且,JIT编译器通常不能花那么多时间在优化(比如内联)上,因此他们为抽象生成的代码比C++编译器更糟糕。(尽管这在将来可能会发生变化,因为.Net Core2.1引入了一个分层的JIT,这意味着它有更多的时间进行优化。)
另一方面,对JIT编译器进行了调整,使其能够很好地工作于真正的代码,而不是微基准(我认为这就是如何得出返回struct
性能较差的结论)。
在我的微基准测试中,使用struct
确实具有更差的性能,但这是因为JIT决定不使用该版本的Function
,而不是因为创建struct
的成本或诸如此类的事情。如果我通过使用[MethodImpl(MethodImplOptions.AggressiveInlining)]
修复了这个问题,那么两个版本的性能都是相同的。
因此,返回struct
在C#中可以是一个零成本的抽象。不过,在C#中发生这种情况的可能性确实比在C++中小。
如果您想知道在out
参数之间切换并返回一个struct
的实际效果,我建议您编写一个更现实的基准,而不是一个微基准,看看结果是什么。(假设我做对了,你使用了一个微基准。)
发布于 2019-06-11 03:18:09
是的,你“可以”,但很难控制。所以,你总是要测试和测量。
一个“零成本抽象”的实际例子:
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
public class App
{
interface IMessages {
string Welcome{ get; }
string Goodbye { get; }
}
partial struct EnglishMessages : IMessages {
public string Welcome {
get { return "Welcome"; }
}
public string Goodbye {
get { return "Goodbye"; }
}
}
partial struct SpanishMessages : IMessages {
public string Welcome {
get { return "Bienvenido"; }
}
public string Goodbye {
get { return "Adios"; }
}
}
static partial class Messages
{
public static SpanishMessages BuildLang {
get { return default; }
}
}
public static void Main() {
Console.WriteLine(Messages.Welcome);
Console.WriteLine(Messages.Goodbye);
}
static partial class Messages
{
public static string Welcome {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get { return GetWelcomeFrom(BuildLang); }
}
public static string Goodbye {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get { return GetGoodbyeFrom(BuildLang); }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetWelcomeFrom<T>()
where T : struct, IMessages
{
var v = default(T);
return v.Welcome;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetWelcomeFrom<T>(T _)
where T : struct, IMessages
{
return GetWelcomeFrom<T>();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetGoodbyeFrom<T>()
where T : struct, IMessages
{
var v = default(T);
return v.Goodbye;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetGoodbyeFrom<T>(T _)
where T : struct, IMessages
{
return GetGoodbyeFrom<T>();
}
}
#region
[StructLayout(LayoutKind.Explicit, Size = 0)]
partial struct EnglishMessages { [FieldOffset(0)] int _; }
[StructLayout(LayoutKind.Explicit, Size = 0)]
partial struct SpanishMessages { [FieldOffset(0)] int _; }
#endregion
}
您可以理解以下代码的技巧:
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
public class App
{
interface IMessage {
string Value { get; }
bool IsError { get; }
}
static class Messages
{
// AggressiveInlining increase the inline cost threshold,
// decreased by the use of generics.
//
// This allow inlining because has low cost,
// calculated with the used operations.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetValue<T>()
where T : struct, IMessage
{
// Problem:
// return default(T).Value
//
// Creates a temporal variable using the CIL stack operations.
// Which avoid some optimizers (like coreclr) to eliminate them.
// Solution:
// Create a variable which is eliminated by the optimizer
// because is unnecessary memory.
var v = default(T);
return v.Value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsError<T>()
where T : struct, IMessage
{
var v = default(T);
return v.IsError;
}
}
// The use of partial is only to increase the legibility,
// moving the tricks to the end
partial struct WelcomeMessageEnglish : IMessage {
public string Value {
get { return "Welcome"; }
}
public bool IsError {
get { return false; }
}
}
partial struct WelcomeMessageSpanish : IMessage {
public string Value {
get { return "Bienvenido"; }
}
public bool IsError {
get { return false; }
}
}
public static void Main() {
Console.WriteLine(Messages.GetValue<WelcomeMessageEnglish>() );
Console.WriteLine(Messages.GetValue<WelcomeMessageSpanish>() );
}
// An struct has Size = 1 and is initializated to 0
// This avoid that, setting Size = 0
#region
[StructLayout(LayoutKind.Explicit, Size = 0)]
partial struct WelcomeMessageEnglish { [FieldOffset(0)] int _; }
[StructLayout(LayoutKind.Explicit, Size = 0)]
partial struct WelcomeMessageSpanish { [FieldOffset(0)] int _; }
#endregion
}
我在CoreClr、Roslyn、Mono和抽象中“测试”了“零成本”:
App.Main()
L0000: push ebp
L0001: mov ebp, esp
L0003: mov ecx, [0xfd175c4]
L0009: call System.Console.WriteLine(System.String)
L000e: mov ecx, [0xfd17628]
L0014: call System.Console.WriteLine(System.String)
L0019: pop ebp
L001a: ret
对于coreclr和roslyn,可以在SharpLab:这里中查看asm。
对于mono (在GNU/Linux中):
mono --aot zerocost.exe
objdump -d -M intel zerocost.exe.so > zerocost.exe.so.dump
cat zerocost.exe.so.dump #Looking for <App_Main>
发布于 2017-09-02 19:49:58
当您返回某项内容时,您总是创建一个新对象--在使用out
参数“就位”时完全保存该步骤。
然后,您有一些编译器不能简单地优化的东西--我必须告诉您一些关于C中严格的混叠规则的内容,但是我不知道C#是否适用于这里。
因此,一般来说,创建一个元组或Abstraction
类型的对象是不可优化的。您特别指定要返回该类型的对象,因此必须通过函数的“一般”编译来创建该对象。您可能会争辩说,编译器知道调用Function
的上下文,并且可以推断不生成对象,而是直接工作,就好像这些引用了以后分配给Abstraction
字段的内容一样,但是这里的混叠规则可能变得非常复杂,这在逻辑上是不可能做到的。
https://stackoverflow.com/questions/46017522
复制相似问题