调用一个空函数需要多长时间?

发布于 2024-12-01 12:41:20 字数 552 浏览 1 评论 0原文

我有一个实现接口的项目列表。对于这个问题,让我们使用这个示例界面:

interface Person
{
  void AgeAYear();
}

有两个类

class NormalPerson : Person
{
  int age = 0;

  void AgeAYear()
  {
    age++;
    //do some more stuff...
  }
}


class ImmortalPerson : Person
{
  void AgeAYear()
  {
    //do nothing...
  }
}

由于其他原因,我需要它们都在列表中。但是对于这个调用,当我循环遍历 Person 列表时,我可能正在调用空函数。 这会对性能产生影响吗?如果是的话,多少? 出于所有意图和目的,空函数是否会被优化掉?


注意:在真实的示例中,ImmortalPerson 还有其他确实有代码的方法 - 它不仅仅是一个不执行任何操作的对象。

I have a list of items implementing an interface. For the question, let's use this example interface:

interface Person
{
  void AgeAYear();
}

There are two classes

class NormalPerson : Person
{
  int age = 0;

  void AgeAYear()
  {
    age++;
    //do some more stuff...
  }
}


class ImmortalPerson : Person
{
  void AgeAYear()
  {
    //do nothing...
  }
}

For other reasons, I need them both of the list. But for this call, when I loop through my list of Persons, I may be calling empty functions. Will this have a performance impact? If so, how much? Will the empty function, for all intents and purposes, be optimized out?


NOTE: In the real example, the ImmortalPerson has other methods that do have code - it is not just an object that does nothing.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

时间海 2024-12-08 12:41:20

这会对性能产生影响吗?

极不可能对性能产生有意义影响。

如果有的话,多少钱?

您可以使用探查器针对您的特定代码路径准确对其进行量化。我们不能,因为我们不知道代码路径。我们可以猜测,并告诉您这几乎肯定无关紧要,因为这极不可能成为您应用程序中的瓶颈(您真的坐在那里在紧密循环中调用 Person.AgeAYear 吗?)。

只有您可以通过拿出分析器并进行测量来准确找到答案。

出于所有意图和目的,空函数是否会被优化掉?

这当然有可能,但也可能不会;它甚至可能在 JITter 的未来版本中发生变化,或者从一个平台到另一个平台发生变化(不同的平台有不同的 JITters)。如果您确实想知道,请编译您的应用程序,然后查看反汇编的 JIT 代码(不是 IL!)。

但我要说的是:这几乎肯定、几乎绝对不值得担心或投入任何时间。除非您在性能关键代码的紧密循环中调用 Person.AgeAYear,否则它不会成为应用程序的瓶颈。您可以花时间在这上面,或者您可以花时间改进您的应用程序。你的时间也有机会成本。

Will this have a performance impact?

Highly unlikely to have a meaningful performance impact.

If so, how much?

You can quantify it exactly for your specific code paths using a profiler. We can not, because we don't know the code paths. We can guess, and tell you it almost surely doesn't matter because this is extremely unlikely to be a bottleneck in your application (are you really sitting there calling Person.AgeAYear in a tight loop?).

Only you can find out precisely by getting out a profiler and measuring.

Will the empty function, for all intents and purposes, be optimized out?

It's certainly possible but it might not; it might even change in future version of the JITter, or change from platform to platform (different platforms have different JITters). If you really want to know, compile your application, and look at the disassembled JITted code (not the IL!).

But I'll say this: this is almost surely, almost definitely not something worth worrying about or putting any time into. Unless you are calling Person.AgeAYear in a tight loop in performance critical code, it's not a bottleneck in your application. You could spend time on this, or you could spend time improving your application. Your time has an opportunity cost too.

过气美图社 2024-12-08 12:41:20
  • 这会对性能产生影响吗?

如果调用该函数,则调用本身可能会花费少量时间。

  • 如果是这样,幅度是多少?

您永远不会注意到任何实际应用程序中的差异 - 与执行任何“实际”工作的成本相比,调用方法的成本非常小。

  • 出于所有意图和目的,空函数是否会被优化掉?

我对此表示怀疑 - 如果该方法位于不同的程序集中,CLR 肯定 可能不会执行这种优化,因为该方法将来可能会发生变化。对于程序集中的方法调用进行这种优化可能是可行的,但这在很大程度上取决于代码,例如在下面的示例中:

foreach (IPerson person in people)
{
    person.AgeAYear();
}

方法调用不能 被优化,因为可能提供不同的 IPerson 实现,它实际上在此方法中执行某些操作。对于IPerson 接口的任何调用肯定会出现这种情况,其中编译器无法证明它始终使用ImmortalPerson 实例。

最终你必须问自己“还有什么选择?”以及“这真的有足够大的影响来保证采取替代方法吗?” 。在这种情况下,影响将非常小 - 我想说,在这种情况下,以这种方式使用空方法是完全可以接受的。

  • Will this have a performance impact?

Yes Probably, if the function is called then the call itself will take a small amount of time.

  • If so, by how much?

You will never notice the difference in any real application - the cost of calling a method is very small compared to the cost of doing any "real" work.

  • Will the empty function, for all intents and purposes, be optimized out?

I doubt it - the CLR definitely probably won't perform this sort of optimisation if the method is in a different assembly as the method may change in the future. It might be feasible that this sort of optimisation is done for method calls inside the assembly but it would depend a lot on the code, for example in the following sample:

foreach (IPerson person in people)
{
    person.AgeAYear();
}

The method call can't be optimised out because a different implementation of IPerson might be supplied which actually does something in this method. This would certainly be the case for any calls against the IPerson interface where the compiler can't prove that its always working with a ImmortalPerson instance.

Ultimately you have to ask yourself "What is the alternative?" and "Does this really have a large enough impact to warrant an alternative approach?" . In this case the impact will be very small - I would say that in this case having an empty method in this way is perfectly acceptable.

携君以终年 2024-12-08 12:41:20

在我看来,您的逻辑似乎有问题,并且无论对性能的影响如何,调用空方法都会散发出糟糕的设计味道。

在您的情况下,您有一个接口,即 Of Person。您声明,要成为一个人,您必须能够变老,正如您的 AgeAYear 方法所强制执行的那样。但是,根据 AgeAYear 方法中的逻辑(或缺乏逻辑),ImmortalPerson 不能老化,但仍然可以是 Person。你的逻辑自相矛盾。您可以通过多种方式解决这个问题,但这是我首先想到的。

实现这一目标的一种方法是设置两个界面:

interface IPerson { void Walk(); }
interface IAgeable : IPerson { void AgeAYear();}

你现在可以清楚地区分,你不必变老才能成为一个人,但为了变老,你必须成为一个人。例如

class ImmortalPerson : IPerson
{
    public void Walk()
    {
        // Do Something
    }
}

class RegularPerson : IAgeable
{
    public void AgeAYear()
    {
        // Age A Year
    }

    public void Walk()
    {
       // Walk
    }
}

,对于您的RegularPerson,通过实现IsAgeable,您还需要实现IPerson。对于您的ImmortalPerson,您只需要实现IPerson

然后,您可以执行如下操作或其变体:

List<IPerson> people = new List<IPerson>();

people.Add(new ImmortalPerson());
people.Add(new RegularPerson());

foreach (var person in people)
{
   if (person is IAgeable)
   {
      ((IAgeable)person).AgeAYear();
   }
}

鉴于上述设置,您仍然强制您的类必须实现 IPerson 才能被视为一个人,但只有在它们也实现时才能老化IAgeable

Your logic seems faulty to me, and regardless of the performace impact, calling an empty method smells of poor design.

In your situation, you have one interface which is Of Person. You are stating that in order to be a person, you must be able to age, as enforced by your AgeAYear method. However, according to the logic in your AgeAYear method (or lack thereof), an ImmortalPerson cannot age, but can still be a Person. Your logic contradicts itself. You could attack this problem a number of ways, however this is the first that pops into my head.

One way to accomplish this would be to set up two interfaces:

interface IPerson { void Walk(); }
interface IAgeable : IPerson { void AgeAYear();}

You can now clearly distinguish that you do not have to age to be a person, but in order to age, you must be a person. e.g.

class ImmortalPerson : IPerson
{
    public void Walk()
    {
        // Do Something
    }
}

class RegularPerson : IAgeable
{
    public void AgeAYear()
    {
        // Age A Year
    }

    public void Walk()
    {
       // Walk
    }
}

Thus, for your RegularPerson, by implementing IsAgeable, you also are required to implement IPerson. For your ImmortalPerson, you only need to implement IPerson.

Then, you could do something like below, or a variation thereof:

List<IPerson> people = new List<IPerson>();

people.Add(new ImmortalPerson());
people.Add(new RegularPerson());

foreach (var person in people)
{
   if (person is IAgeable)
   {
      ((IAgeable)person).AgeAYear();
   }
}

Given the setup above, your still enforcing that your classes must implement IPerson to be considered a person, but can only be aged if they also implement IAgeable.

星軌x 2024-12-08 12:41:20

编译器无法了解将调用这两个函数中的哪一个,因为这是在运行时设置的函数指针。

您可以通过检查 Person 内部的某些变量、确定其类型或使用dynamic_cast 来检查它来避免它。如果该函数不需要调用,则可以忽略它。

调用函数由几条指令组成:

  • 将参数压入进程堆栈(本例中没有)
  • 压入返回地址和一些其他数据
  • 跳转到函数

当函数结束时:

  • 从函数跳回,
  • 更改堆栈指针以有效删除被调用函数的堆栈帧

对您来说可能看起来很多,但也许它只是检查变量类型并避免调用的成本的两倍或三倍(在另一种情况下,您检查了一些变量和可能的跳转,这几乎与调用一个你只会节省返回成本。但是,你会检查需要调用的函数,所以最终你可能不会节省任何东西!)

在我看来,你的算法有很多。与单纯的函数调用相比,它对代码性能的影响更大。所以,不要因为这样的小事而困扰自己。

大量(也许数百万)调用空函数可能会对程序的性能产生一些影响,但如果发生这种情况,则意味着您在算法上做了一些错误的事情(例如,认为应该将 NormalPersons 和 ImmortalPersons 放在同一个中)列表)

There is no way for the compiler to understand which of the two functions is going to be called, as that is a function pointer set at run time.

You could avoid it, through checking of some variable inside Person, determining its type or use of dynamic_cast to check it. If the function didn't need calling, then you can ignore it.

Calling a function consists of a few instructions:

  • pushing the arguments on the process stack (none in this case)
  • pushing return addresses and a few other data
  • jumping to the function

And when the function ends:

  • jumping back from the function
  • changing stack pointer to effectively remove stack frame of the called function

It may look a lot to you, but perhaps it is just maybe twice or three times the cost of checking the type of the variable and avoiding the call (in the other case, you have a check of some variable and a possible jump, which takes almost the same time as calling an empty function. You would only be saving the return cost. However, you do the check for the functions that DO need to be called, so in the end you are probably not saving anything!)

In my opinion, your algorithm has a much much greater impact on the performance of your code than a mere function call. So, don't bug yourself with small things like this.

Calling empty functions in large numbers, maybe millions, may have some effect on your program's performance, yet if such a thing happens, it means that you are doing something algorithmically wrong (for example, thinking that you should put NormalPersons and ImmortalPersons in the same list)

扶醉桌前 2024-12-08 12:41:20

在相对现代的工作站上,C# 委托或接口调用大约需要2 纳秒。用于比较:

  • 分配一个小数组:10 纳秒
  • 分配一个闭包:15 纳秒
  • 获取一个无竞争的锁:25 纳秒
  • 字典查找(100 个短字符串键):35 纳秒
  • DateTime.Now (系统调用):750纳秒
  • 通过有线网络进行数据库调用(以小表计): 1,000,000 纳秒(1 毫秒)

所以除非你优化紧密循环,方法调用不太可能成为瓶颈。如果您正在优化紧密循环,请考虑更好的算法,例如在处理事物之前在 Dictionary 中索引事物。

我在 3.40 GHz 的 Core i7 3770 上测试了这些,使用 LINQPad 32 位并启用了优化。但由于内联、优化、寄存器分配和其他编译器/JIT 行为,方法调用的时间将根据上下文而有很大差异。 2 纳秒只是一个大概数字。

就您而言,您正在循环列表。循环开销可能会主导方法调用开销,因为 内部循环涉及大量方法调用。在您的情况下,性能不太可能成为问题,但如果您有数百万个项目和/或您定期需要更新它们,请考虑更改表示数据的方式。例如,您可以有一个全局递增的“年份”变量,而不是递增每个人的“年龄”。

On a relatively modern workstation, a C# delegate or interface call takes around 2 nanoseconds. For comparison:

  • Allocating a small array: 10 nanoseconds
  • Allocating a closure: 15 nanoseconds
  • Taking an uncontested lock: 25 nanoseconds
  • Dictionary lookup (100 short string keys): 35 nanoseconds
  • DateTime.Now (system call): 750 nanoseconds
  • Database call over wired network (count on a small table): 1,000,000 nanoseconds (1 millisecond)

So unless you are optimizing a tight loop, method calls are not likely to be a bottleneck. If you are optimizing a tight loop, consider a better algorithm, such as indexing things in a Dictionary before processing them.

I tested these on a Core i7 3770 at 3.40 GHz, using LINQPad 32-bit with optimization turned on. But due to inlining, optimization, register allocation, and other compiler/JIT behavior, the timing of a method call will vary wildly depending on context. 2 nanoseconds is just a ballpark figure.

In your case, you are looping through lists. The looping overhead is likely to dominate the method call overhead, since looping internally involves lots of method calls. Performance is not likely to be an issue in your case, but if you have millions of items and/or you regularly need to update them, consider changing how you represent the data. For example, you could have a single "year" variable that you increment globally instead of incrementing everyone's "age".

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文