2

假设您有这样的 C# 代码:

const string ABC = "ABC";
const string XYZ = "XYZ";

var result = ABC + ":" + XYZ;

此类代码由编译器优化,因此结果变量中有一个常量字符串文字“ABC:XYZ”,而不是在运行时实际连接 3 个字符串。

现在我想介绍一个辅助函数,它将对用户隐藏该连接(以使代码更健壮且不易出错):

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static string Combine(string first, string second) => first + ":" + second;

const string ABC = "ABC";
const string XYZ = "XYZ";

var result = Combine(ABC, XYZ);

在这种情况下,我看到该方法实际上是内联在调用方法中的,但在反汇编中我可以清楚地看到它string.Concat(string a, string b, string c)在运行时调用,而不是有一个常量字符串文字“ABC:XYZ”。

我知道在编译过程中速度和质量之间存在平衡,但是有什么技巧可以让该Combine方法作为 C++ 宏工作以避免在运行时出现多余的字符串连接?

PS 引入只计算一次的静态只读字符串并不是问题的答案。

4

2 回答 2

3

了解MethodImplOptions.AggressiveInlining提示及其含义/用途是很有用的。

我的理解是方法内联(无论程序员是否暗示/希望积极地完成) - 都是关于调用站点(即,在你的情况下,关于“由谁/在哪里”调用你的组合),但是不是关于什么构成(可能是内联的)方法的主体。

换句话说,当你重构你的表达式时

... ABC + ":" + XYZ ...

(显然,这正是恒定折叠优化的“完美”/名义候选者)

进入

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static string Combine(string first, string second) => first + ":" + second

接着

(在呼叫站点)

... Combine(ABC, XYZ) ...

你真的“告诉”了一些与编译器完全不同的东西:

“我想要一个接受两个字符串并返回第三个字符串的静态方法,我想在这个调用站点上调用它,哦,请尝试为我内联它。”

因为在您的情况下,Combine 显然是一个纯粹的非递归函数,它对调用堆栈不做任何事情,无论在哪里使用实际参数(无论是否是那些常量)调用它都不能“就地”重写,因此内联确实是可行的它的调用——但这仍然没有说明可以在 Combine 方法的主体本身上进行哪些其他额外的优化这里相当于(在 lambda 脱糖之后)

return first + ":" + second;

(并因此编译为

return string.Concat(first, second);

正如你所注意到的)

毕竟,如果你的代码的其他部分有一个依赖于反射(通过 System.Reflection 的功能)的逻辑呢?它仍然需要在某个地方声明为成员并附有一个主体,不是吗?(再次,无论它是否可以,“作为奖励”,在这里或那里内联,等等)

从那里,“附着在它上面的物体”意味着必须有一个,而且完全是一个,用 CIL/MSIL 等表示。

这就是我如何在您的 Combine 方法上看到这个 [MethodImpl(MethodImplOptions.AggressiveInlining)] 只是一个提示,“嘿,拜托,让我们尽可能地避免在这里和那里调用 Combine 的唯一、愚蠢的堆栈框架。” ——除此之外别无他求。

于 2016-09-19T21:22:27.033 回答
0

可以使用编译时编织来做到这一点。这里的想法是,您将找到您的方法的所有调用站点(不要内联它们!)并在编译时用连接字符串作为操作数的 ldtoken 操作码替换它们。

这些工具将有助于实现您的目标。

•PostSharp •Aspect.NET。依靠凤凰。•AspectDNG。依靠塞西尔。• ComposeStar / StarLight • Gripper Loom.NET •Phx.Morph/Wicca。依靠凤凰。

于 2016-09-26T20:59:07.760 回答