C#中的内联函数是什么?

303

13
终于有可能了 - 看看我的回答。 - konrad.kruczynski
1
对于好奇的人,[可以看看这个VS扩展](https://marketplace.visualstudio.com/items?itemName=StephanZehetner.InliningAnalyzer)。 - TripleAccretion
13个回答

431

在 .NET 4.5 中,CLR 允许使用 MethodImplOptions.AggressiveInlining 值提示/建议方法内联。它也在 Mono 的主干版本中提供(今天提交)。

// The full attribute usage is in mscorlib.dll,
// so should not need to include extra references
using System.Runtime.CompilerServices; 

...

[MethodImpl(MethodImplOptions.AggressiveInlining)]
void MyMethod(...)

1. 之前使用的是“force”这个词。我会尝试澄清这个术语。在注释和文档中,如果可能应该内联方法。 特别是考虑到 Mono(它是开源的),有一些针对内联或更一般问题(如虚函数)的特定技术限制。总的来说,是的,这是给编译器的一个提示,但我想那就是被要求的。


4
在大多数情况下,通过覆盖JIT编译器的启发式算法来实现此操作已经足够了,虽然它可能仍不是一个force inline。 - CodesInChaos
8
一种适用于所有 .NET 版本的不同方法是:将稍微有些过长的方法拆分成两个方法,一个方法调用另一个方法,两个方法的 IL 都不超过 32 字节。这样做的效果就好像原始方法被内联了一样。 - Rick Sladkey
4
这并不是强制执行“内联”,只是试图与JIT交流,并告诉它程序员真正想在这里使用内联,但 JIT 对此有最终决定权。 因此MSDN指出:如果可能的话,应该将方法内联。 - Orel Eraki
16
相比之下,即使是编译器特定的C++内联建议,也并不会真正强制内联:并非所有函数都可以内联(从根本上说,像递归函数这样的东西很难,但还有其他情况)。因此,是的,这种“不完全”的强制是很常见的。 - Eamon Nerbonne
2
我认为即使加上“如果可能”的条件也有点过了;有很多情况下,Jitter 可能会被扩展以处理甚至在某一天也许会被内联的情况,但目前似乎只是忽略了大小限制的问题。但由于我们可以预期在未来几年中在这方面会有所改变,因此最好只说“使其更积极地决定是否内联”。 - Jon Hanna
显示剩余3条评论

91

内联方法是编译器的一种优化方式,其中函数的代码被嵌入到调用者中。

C#中没有执行此操作的机制,在支持它们的语言中应谨慎使用--如果您不知道何时应该在某个位置使用它们,则不应该使用。

编辑:为了澄清,需要谨慎使用它们有两个主要原因:

  1. 在不必要的情况下使用内联很容易使二进制文件变得过大
  2. 从性能角度来看,编译器往往比您更知道何时应该进行内联处理

最好让事情保持原样,让编译器完成其工作,然后进行性能分析并确定内联是否是最佳方案。当然,某些事情只是理所当然地进行内联(特别是数学运算符),但是让编译器处理通常是最佳实践。


39
通常情况下,我认为编译器自己处理内联是可以的。但有些情况下,我希望覆盖编译器的决定,手动决定是否内联某个方法。 - mmmmmmmm
7
这样做是不好的实践,因为它会破坏模块化和访问保护。(想象一下在类中调用私有成员并从代码的不同位置调用的内联方法!) - mmmmmmmm
10
@Poma 这是一个奇怪的理由来使用内联。我非常怀疑它会证明是有效的。 - Chris Shouts
70
有关编译器最懂的争论是错误的。它不会内联任何大于32 IL字节的方法,这对于像我的项目以及上面Egor的项目一样,存在经过分析识别出的性能瓶颈,我们完全无法解决这些问题,非常糟糕。除了手动复制和内联代码外,我们无能为力。简而言之,当您真正需要提高性能时,这是一个可怕的处境。 - bright
8
内联不像表面看起来那样简单。内存有快速访问的缓存,当前代码部分存储在这些缓存中,就像变量一样。加载下一个缓存行的指令是缓存未命中,成本可能是单个指令的10倍或更多。最终它必须加载到L2缓存中,这甚至更加昂贵。因此,膨胀的代码可能会导致更多的缓存未命中,而常驻同一L1缓存行中的内联函数可能导致更少的缓存未命中和更快的代码。对于.Net来说,情况更为复杂。 - user334911
显示剩余6条评论

63

更新:根据konrad.kruczynski的答案,以下内容适用于.NET版本包括4.0及以前版本。

您可以使用MethodImplAttribute类防止一个方法被内联编译...

[MethodImpl(MethodImplOptions.NoInlining)]
void SomeMethod()
{
    // ...
}

...但是没有办法相反地强制使其成为内联。


2
很有趣,但为什么要禁止一个方法被内联呢?我花了一些时间盯着显示器,但我无法想出为什么内联会造成任何伤害。 - Camilo Martin
3
如果你收到一个调用栈(即NullReferenceException),显然顶部方法没有抛出这个异常。当然,它的其中一个调用可能已经抛出了异常。但是哪一个呢? - dzendras
20
根据方法是否被内联,GetExecutingAssemblyGetCallingAssembly可能会给出不同的结果。强制一个方法为非内联可以消除任何不确定性。 - stusmith
@CamiloMartin 若干年后,一个导致内联造成伤害的示例是在动态加载程序集时。这里有一个示例链接 - Josh T.
“...没有办法做相反的操作并强制它内联...”要明确,有 [MethodImpl(MethodImplOptions.AggressiveInlining)],但是虽然 NoInlining 被决定性地执行,但后者仅向 JIT 提供建议。此外,除了 @stusmith 已经提到的语义差异之外,我还会添加另一个语义差异,如下所示... - Glenn Slayden
任何操作其in,refout(托管指针)参数的函数,如果不固定,就需要负责确保每个参数在垃圾回收中可见。对于非内联方法,所需的堆栈或使用rcx``rdx寄存器(垃圾回收也知道)是传递参数时的额外副产品。然而,在x64 JIT中,这种“自由”的可见性并不是保证的。当进行内联处理时,内联合格方法的托管指针参数可能完全从堆栈中优化出去,因此不在垃圾回收的可见性范围内。唯一确保GC正确性的方法是将该方法标记为“NoInlining”。 - Glenn Slayden

38

你把两个不同的概念混淆了。函数内联是编译器优化,对语义没有影响。无论函数是否被内联,它的行为都是相同的。

另一方面,lambda函数纯粹是一个语义概念。没有要求如何实现或执行它们,只要它们遵循语言规范中设定的行为。如果JIT编译器认为合适,它们可以被内联,否则就不会被内联。

C# 中没有内联关键字,因为这是一种通常可以留给编译器优化的技术,特别是在JIT编译的语言中。JIT编译器有访问运行时统计信息的权限,使其能够更有效地决定内联什么,而当你编写代码时,你不能做出任何改变。 :)


22
“一个函数无论是否内联,其行为都是相同的。”有一些罕见的情况不符合这个说法:即希望了解堆栈跟踪的日志记录函数。但我不想过分削弱你的基本陈述:它通常是正确的。 - Joel Coehoorn
1
"而且无论如何,你都无能为力。" - 不是真的。即使我们不把对编译器的“强烈建议”算作任何事情,你也可以防止函数被内联。 - BartoszKP

27

Cody说得很对,但我想举个例子来说明什么是内联函数。

假设你有这样的代码:

private void OutputItem(string x)
{
    Console.WriteLine(x);

    //maybe encapsulate additional logic to decide 
    // whether to also write the message to Trace or a log file
}

public IList<string> BuildListAndOutput(IEnumerable<string> x)
{  // let's pretend IEnumerable<T>.ToList() doesn't exist for the moment
    IList<string> result = new List<string>();

    foreach(string y in x)
    {
        result.Add(y);
        OutputItem(y);
    }
    return result;
}
< p > Just-In-Time优化器可以选择修改代码,以避免重复将调用OutputItem()放入堆栈中,因此它就好像您写的代码是这样的:

public IList<string> BuildListAndOutput(IEnumerable<string> x)
{
    IList<string> result = new List<string>();

    foreach(string y in x)
    {
        result.Add(y);

        // full OutputItem() implementation is placed here
        Console.WriteLine(y);   
    }

    return result;
}

在这种情况下,我们会说OutputItem()函数被内联。请注意,即使从其他地方调用OutputItem(),它也可能会执行此操作。 < p >编辑以显示更有可能被内联的场景。


12
只是为了澄清一下,实现内联的是JIT,而不是C#编译器。 - Marc Gravell
还要注意的是,JIT 最初至少会“更喜欢”内联静态方法,即使跨越程序集边界。因此,一种旧式的优化技术是将方法标记为静态的。尽管社区已经不再赞同这种做法,但直到今天,我仍然会选择内联那些内部、非多态性质且执行成本通常比相关堆栈填充+溢出低的方法。 - Shaun Wilson

22
你是指 C++ 中的内联函数吗?其中普通函数的内容会自动复制到调用点中。最终效果是在调用函数时实际上没有发生任何函数调用。
示例:
inline int Add(int left, int right) { return left + right; }

如果是这样的话,那么没有C#对应的功能。

或者你是指在另一个函数内声明函数吗?如果是的话,那么通过匿名方法或lambda表达式,C#可以支持这种操作。

例如:

static void Example() {
  Func<int,int,int> add = (x,y) => x + y;
  var result = add(4,6);  // 10
}

8

没错,唯一的区别就是它返回一个值。

简化(不使用表达式):

List<T>.ForEach 接受一个操作,不需要返回结果。

因此,一个 Action<T> 委托就足够了,比如:

List<T>.ForEach(param => Console.WriteLine(param));

这句话的意思是:

List<T>.ForEach(delegate(T param) { Console.WriteLine(param); });

不同之处在于,参数类型和委托声明是由使用推断出来的,并且简单的内联方法不需要括号。

List<T>.Where接受一个期望结果的函数。

因此,需要一个Function<T, bool>

List<T>.Where(param => param.Value == SomeExpectedComparison);

这与以下代码相同:

List<T>.Where(delegate(T param) { return param.Value == SomeExpectedComparison; });

您可以将这些方法声明为内联方法,并将它们分配给变量,例如:

IE:

Action myAction = () => Console.WriteLine("I'm doing something Nifty!");

myAction();

或者

Function<object, string> myFunction = theObject => theObject.ToString();

string myString = myFunction(someObject);

我希望这可以帮到您。

3
"最好不要碰这些东西,让编译器自己处理"(Cody Brocious)这种说法完全是胡说八道。我从事高性能游戏代码编程已有20年之久,但我从未遇到过任何一个编译器“聪明到足以知道哪些代码应该被内联(函数)”,且在没有“inline”提示的情况下,编译器就没有全部所需信息判断哪些函数应始终内联或不内联。当然,如果该函数很小(如访问器),那么它可能会自动内联,但如果它是几行代码呢?无稽之谈,编译器无法得知,对于优化代码(除算法外),你不能仅仅把它留给编译器处理。"

4
胡言乱语,编译器无法知道。JIT编译器会实时分析函数调用... - Rag
3
.NET的JIT被认为比编译时的两遍静态分析更好地优化运行时间。对于“老派”的程序员来说,难以理解的是JIT而不是编译器负责生成本机代码,JIT而不是编译器负责内联方法。 - Shaun Wilson
1
我可以编译你调用的代码,而不需要我的源代码,JIT 可能会选择内联调用。通常情况下我会同意,但我认为作为人类,你已经无法超越工具了。 - Shaun Wilson

3
有时候,我想强制将代码嵌入到行内。
例如,如果我有一个复杂的例程,在其中有大量的决策在高度迭代的块内进行,并且这些决策导致类似但稍有不同的操作被执行。例如,考虑一个复杂(非数据库驱动)的排序比较器,其中排序算法根据许多不相关的标准对元素进行排序,就像在为快速语言识别系统按照语法和语义标准对单词进行排序时所做的那样。我倾向于编写帮助函数来处理这些操作,以保持源代码的可读性和模块化。
我知道这些帮助函数应该是嵌入式的,因为如果代码永远不需要被人理解,那么代码就应该这样编写。在这种情况下,我肯定希望确保没有函数调用开销。

1

我知道这个问题是关于C#的。然而,在.NET中,你可以使用F#编写内联函数。请参见:F#中使用`inline`


我是从这里重定向过来的:http://stackoverflow.com/questions/13764789/how-to-inline-methods-in-net-3-5 - Goswin Rothenthal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接