我通常用具有零成本抽象概念的语言编程,如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
函数版本的问题在于我真的不想使用参数作为返回值,因为它似乎更像是语言限制的 hack 。
所以,最后我不确定我是否只是没有使用正确的配置,因此JIT可以使用零成本抽象,或者这在C#中根本不可能或不能保证。
答案 0 :(得分:3)
首先,我认为说语言“具有零成本的抽象”是没有道理的。考虑功能的抽象。是零成本吗?一般来说,只有内联它才是零成本。尽管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
,这并不是因为创建{{1 }}或类似的东西。如果我使用struct
修复了该问题,那么两个版本的性能都相同。
因此,返回[MethodImpl(MethodImplOptions.AggressiveInlining)]
在C#中可能是零成本的抽象。尽管确实如此,与C ++相比,C#发生这种情况的可能性较小。
如果您想了解在struct
参数之间切换并返回out
的实际影响,建议您编写一个更现实的基准测试,而不是微基准测试,然后看看结果是什么。 (假设我没错,您使用了微基准测试。)
答案 1 :(得分:1)
当您返回某些内容时,您始终会创建一个新对象 - 当您使用2017-09-02 14:46:39.708 whatever[67890:8535793] number of rows in section: 0
2017-09-02 14:46:39.714 whatever[67890:8535793] number of rows in section: 0
2017-09-02 14:46:39.715 whatever[67890:8535793] number of rows in section: 0
2017-09-02 14:46:40.448 whatever[67890:8535846] I return 0: 0
2017-09-02 14:46:41.174 whatever[67890:8535846] I return results: 20
参数“就地”工作时,您可以完全保存该步骤。
然后,你有一些你的编译器无法简单优化的东西 - 我必须告诉你一些关于C中的严格别名规则,但我不知道C#是否足以知道类似的事情是否适用于此。
因此,通常,创建元组或out
类型的对象是不可优化的。您明确指定要返回该类型的对象,因此该对象必须通过该函数的“常规”编译来创建。您可能会争辩说编译器知道调用Abstraction
的上下文,并且可以推断出不生成对象是可以的,但是直接工作就好像这些是对您分配{{1}字段的内容的引用一样以后,但是这里别名规则可能会变得非常复杂,而这在逻辑上通常是不可能的。
答案 2 :(得分:1)
是的,您“可以”;但是很难控制。因此,您始终必须进行测试和测量。
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。
对于单声道(在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>