我通常用具有零成本抽象概念的语言编程,如C ++和Rust。
目前我正在使用C#语言的项目。所以我想知道我是否可以在不影响性能的情况下安全地创建抽象和更高级别的代码。
这是可能的C#或性能关键代码我应该尽可能做低级代码吗?
就像我在代码中遇到的一个例子(不要过多关注这个例子,我的问题是更高级别),我需要一个能返回多个值的函数,为此,我的第一个方法是使用一个元组,所以这样的事情:
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#中根本不可能或不能保证。
首先,我认为说语言“具有零成本抽象”并不合理。考虑功能的抽象。是零成本吗?一般来说,只有在内联时它才是零成本。虽然C ++编译器往往非常擅长内联函数,但它们并没有内联所有函数,所以C ++中的函数严格来说并不是零成本抽象。但这种差异在实践中很少见,这就是为什么你通常可以认为函数是零成本的。
现在,现代C ++和Rust的设计和实现方式使它们尽可能地使抽象成本为零。这在C#中是不同的吗?的种类。 C#的设计并不是专注于零成本抽象(例如,在C#中调用lambda总是涉及实际上是虚拟调用;在C ++中调用lambda不会,这使得它更容易实现零成本)。此外,JIT编译器通常不能花费太多时间在内联等优化上,因此它们会产生比C ++编译器更糟糕的抽象代码。 (虽然这可能会在未来发生变化,因为.Net Core 2.1 introduced a tiered JIT,这意味着它有更多的时间进行优化。)
另一方面,JIT编译器被调整为适用于实际代码,而不是微基准测试(我假设你是如何得出结论,返回struct
的性能更差)。
在我的微基准测试中,使用struct
确实表现较差,但这是因为JIT决定不插入那个版本的Function
,这不是因为创建一个struct
的成本,或类似的东西。如果我通过使用[MethodImpl(MethodImplOptions.AggressiveInlining)]
修复它,两个版本都达到了相同的性能。
因此,返回struct
可能是C#中的零成本抽象。虽然在C#中发生这种情况的可能性比在C ++中更小。
如果你想知道在out
参数和返回struct
之间切换的实际效果是什么,我建议你写一个更现实的基准,而不是微基准,看看结果是什么。 (假设我使用了微基准测试,我说得对。)
当您返回某些内容时,您始终会创建一个新对象 - 当您使用out
参数“就地”工作时,您可以完全保存该步骤。
然后,你有一些你的编译器无法简单优化的东西 - 我必须告诉你一些关于C中的严格别名规则,但我不知道C#是否足以知道类似的东西是否适用于此。
因此,通常,创建元组或Abstraction
类型的对象是不可优化的。您明确指定要返回该类型的对象,因此该对象必须通过该函数的“常规”编译来创建。您可以争辩说编译器知道调用Function
的上下文,并且可以推断出可以不生成对象但是直接工作就好像这些是对你将Abstraction
的字段分配给以后的东西的引用,但这里是别名规则可能会变得非常复杂,而这在逻辑上通常是不可能做到的。
是的你可以”;但是很难控制。所以,你总是要测试和测量。
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:Here。
对于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>