C#是否具有零成本抽象? 我通常用具有零成本抽象概念的语言编程,如C++和RIST.
目前我在一个使用C语言的项目中工作。所以我想知道我是否可以安全地创建抽象和更高级别的代码,而不会影响性能 这在C#中可能吗?或者对于性能关键型代码,我应该尽可能地编写低级代码 正如我在代码中遇到的一个示例(不要过多地关注这个示例,我的问题更高层次),我需要一个返回多个值的函数,为此,我的第一种方法是使用元组,如下所示:C#是否具有零成本抽象? 我通常用具有零成本抽象概念的语言编程,如C++和RIST.,c#,optimization,compiler-optimization,jit,C#,Optimization,Compiler Optimization,Jit,目前我在一个使用C语言的项目中工作。所以我想知道我是否可以安全地创建抽象和更高级别的代码,而不会影响性能 这在C#中可能吗?或者对于性能关键型代码,我应该尽可能地编写低级代码 正如我在代码中遇到的一个示例(不要过多地关注这个示例,我的问题更高层次),我需要一个返回多个值的函数,为此,我的第一种方法是使用元组,如下所示: public (int, int, float) Function(); 或将此元组抽象为结构: public struct Abstraction { int value1;
public (int, int, float) Function();
或将此元组抽象为结构:
public struct Abstraction { int value1; int value2; float value3; };
public Abstraction Function();
我所期望的是编译器将优化元组
或抽象结构
并直接使用原语值。但我发现,使用out
参数编写代码可以提高性能:
public void Function(out int value1, out int value2, out float value3);
我猜原因是因为在out
函数中,没有Tuple
或抽象结构的创建
out
函数版本的问题是,我真的不喜欢使用参数作为返回值,因为它更像是对语言限制的一种攻击
因此,最后我不确定我是否只是没有使用正确的配置,以便JIT可以使用零成本抽象,或者这在C#中根本不可能或不保证 当您返回某个内容时,您总是创建一个新对象–当您使用out
参数“就地”工作时,您完全保存了该步骤
然后,你的编译器不能简单地优化掉一些东西——我必须告诉你一些关于C语言中严格的别名规则,但我对C语言的了解还不足以知道这里是否适用类似的东西
因此,一般来说,创建tuple或抽象类型的对象是不可优化的。您特别指定要返回该类型的对象,因此该对象必须由函数的“常规”编译创建。你可以说编译器知道调用函数
的上下文,并且可以推断不生成对象是可以的,但是直接工作就像这些引用了你以后分配给抽象
的字段一样,但是这里的别名规则可能会变得非常复杂,一般来说,这在逻辑上是不可能做到的。首先,我认为说语言“具有零成本抽象”是没有意义的。考虑函数的抽象性。是零成本吗?一般来说,只有当它是内联的时,它才是零成本的。虽然C++编译器对内联函数非常擅长,但它们并不内联所有函数,所以C++中的函数严格来说不是零成本抽象。但是这种差异在实践中很少有,这就是为什么你通常会认为函数是零成本的。
现在,现代C++和Rust的设计和实现方式是尽可能地使抽象零成本。这在C#中不同吗?有点C++没有被设计成集中于零成本抽象(例如,调用C中的lambda总是涉及什么是有效的虚拟调用;在C++中调用lambda不,这使得它更容易使其成为零成本)。而且,JIT编译器通常不能花太多时间在优化,比如内联,因此它们比C++编译器产生更坏的抽象代码。(尽管这在将来可能会改变,因为,这意味着它有更多的时间进行优化。)
另一方面,JIT编译器经过了调整,可以很好地处理真实代码,而不是微基准(我假设这就是为什么您得出结论,返回struct
的性能更差)
在我的microbenchmark中,使用struct
确实性能较差,但这是因为JIT决定不内联该版本的函数,而不是因为创建struct
的成本,或者诸如此类的东西。如果我通过使用[MethodImpl(methodimpoptions.aggressiveinline)]
修复了这个问题,那么这两个版本都获得了相同的性能
因此,在C#中,返回struct
可以是零成本的抽象。虽然C语言中发生的几率比C++中的几率小。
如果您想知道在out
参数之间切换并返回struct
的实际效果,我建议您编写一个更现实的基准,而不是一个微基准,然后看看结果如何。(假设我没弄错,你用的是微基准。)是的,你“可以”;但是很难控制。所以,你总是需要测试和测量
“零成本抽象”的实例:
对于coreclr和roslyn,您可以在SharpLab中查看asm:
对于mono(在GNU/Linux中):
mono--aot zerocost.exe
objdump-d-M英特尔zerocost.exe.so>zerocost.exe.so.dump
cat zerocost.exe.so.dump#寻找
这看起来像是微观优化。使用一种方法而不是另一种方法的任何性能成本都可能可以忽略不计,并且与算法本身的整体性能相比可能无关紧要。结构本身就是一种抽象。如果该方法不能内联,那么抖动会将该方法重写为void函数(out-Abstraction-retval)。就像C或C++编译器必须做的那样。有很多糟糕的基准测试,这是非常快的代码,很难衡量。就在今天,我看到有人使用了一个流行的基准测试工具,它保证了一个微基准测试的小方法永远不会内联。幻影数字。要想找到罐底,仍然需要摸索机器代码和优化器,C#有更多的罐。“分层JIT”的评论可以帮助我们
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
}
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