通过返回值还是修改传递的参数引用来实现,哪种方式更快?

9
在我撰写的程序中,我需要在函数间传递大型数据结构(图片)。 我需要尽可能快地编写代码,并在不同的操作系统上使用(因此,我无法针对所有测试用例进行分析)。 我经常写以下形式的代码...
void foo() {
  ImageType img = getCustomImage();
}

ImageType getCustomImage() {
  ImageType custom_img;
  //lots of code
  return custom_img;
}

据我所知,代码行 ImageType img = getCustomImage(); 将会调用一个复制构造函数来使用 custom_img 函数的返回值作为它的参数。维基百科称,有些编译器甚至会再次执行这个操作,对于一个初始的临时变量!

我的问题是:通常情况下,通过引用传递而不是返回值来绕过这个开销(图像的复制构造函数很昂贵)是否更快呢...

void foo() {
  ImageType img;
  getCustomImage(img);
}

void getCustomImage(ImageType &img) {
  //code operating directly on img
}

有人告诉我,如果编译器支持返回值优化,则不应该有任何区别。这是真的吗?现在,在合理范围内,我可以假设这一点吗?当速度很重要时,我该如何构建我的程序结构?


1
你为什么打上C的标签?根据你提到的复制构造函数和在getCustomImage函数签名中使用引用传递的语法,这只适用于C++。 - Vicky
根据您的ImageType构造函数,即使没有返回值优化,这两种形式可能更或少等效。这就是为什么“哪个更快”的唯一正确答案是“分析”。如果您不能在所有平台上都进行,则选择您喜欢的...主要问题可能是普遍适用的。 - Dennis Zickefoose
离题一点,我想说一个好的方法,可以避开你所有的烦恼,那就是重新审视“ImageType”的定义。如果这个类变成一个小型、轻量级的处理程序,用于动态管理存储(比如成员“vector”),并且使用C++11的移动语义,那么你可以保持代码不变(按值返回),并获得良好的性能表现。 - Kerrek SB
4个回答

13

您应该编写易于维护的代码,编译器在大多数情况下都能很好地处理性能问题。如果您感到事情变慢了,那么请测量性能,并在确定瓶颈位置后尝试找出如何改进。

您说得对,在逻辑上,代码触发不同的复制构造:从custom_img到返回的临时对象,然后到调用方代码中的img对象,但事实是两个副本都将被省略。

按值返回默认构造+引用传递的特定情况下,我所知道的所有调用约定都通过让调用者分配内存并传递一个隐藏指针给被调用者来实现按值返回,这有效地实现了您要尝试做的事情。因此,从性能角度来看,它们基本上是等效的。

我曾在这两篇博客文章中写过关于函数参数和返回值的值语义:

编辑:我故意避免了编译器无法应用NRVO的情况的讨论,原因是任何可以接受对象引用进行处理的函数fvoid f(T& out) { /* code */ }都可以轻松转换为一个函数,其中NRVO对于编译器来说是容易实现的,通过简单的转换返回值:T f() { T out; /* code */ return out; }


就像你所说的那样,但没有人比唐纳德·克努斯表达得更好:“我们应该忘记小效率,大约97%的时间:过早优化是万恶之源。” :) - Some programmer dude
@JoachimPileborg:我不太确定你的引用是否完全支持David所说的,它是相关的,但是...我期望程序员能够将他们自己的“古怪”想法识别为剩下的3%。 - Matthieu M.
我想补充一点,大多数情况下,“默认构造”的对象是没有意义的。构造函数应该初始化类不变式,因此大多数默认构造函数都是设计错误(不比空指针更好:臭名昭著的十亿美元错误)。当您获得一个对象时,您希望它已经准备就绪,这是构造函数的约定。 默认构造函数(如空指针)违反了该约定。而没有默认构造函数,"out-parameters"的用法就不可能了(它们会变成in/out)。 - Matthieu M.
@JoachimPileborg:继续引用:“然而,我们不应该放弃在那关键的3%中的机会。一个好的程序员不会被这样的推理所麻痹,他将明智地仔细查看关键代码;但前提是必须先确定哪些代码是关键的。” - Mike Seymour
1
@MatthieuM。完全同意,不仅如此,甚至在默认构造本身有意义的情况下(例如返回一个元素向量的函数,空向量是一个完整构建的对象),即使在那些情况下,它也会让代码变得复杂。函数无法控制传递给它的向量是否为空,因此为了符合其后置条件,函数应该处理非空情况并进行清除操作,这将需要更多的代码来维护和执行。 - David Rodríguez - dribeas

1

由于您的图像是大型数据结构,我建议该函数应返回指向图像的指针。您也可以使用引用(在机器级别上是指针),但我认为指针更适合这个目的。

我比较熟悉C语言而不是C++,所以可能会有错误。

重要问题是何时以及由谁来释放您的图像。


2
如果使用指针,它们需要是某种智能指针。 - balki
回到最初的问题,Qt是一个带有QImage类的C++库。 - Basile Starynkevitch
@James:因为裸指针会邀请内存泄漏在你的代码上跳舞,直到它可怕地死去。 - Matthieu M.
1
@Matthieu M:我想知道这个主张中的“需要”部分。 - James
@James:我认为这是一个直白的陈述,可以加以改进。然而,在实践中,原始指针会引发疑问:开发人员是否使用原始指针是因为它不拥有资源,还是她忘记了我们的约定,实际上它确实拥有资源?系统地使用“智能”指针(并具有愚蠢的pointer类)可以清楚地记录代码中的所有权,不仅是其种类(如果有的话),还包括其缺失。 - Matthieu M.

1

至少,如果你的目标是针对相当现代的编译器和像Windows、MacOS、Linux或*BSD这样的典型操作系统,那么你可以相当有把握地指望它们实现RVO/NRVO。换句话说,你必须非常努力才能找到足够的差异来关心——或者最有可能根本没有。

根据所涉及数据的使用方式,如果存在速度差异,传递/返回对象与使用引用几乎同样容易。你可能想阅读David Abrahams的文章了解更多信息。


0
通常当看到问题“哪个更快?”时,我建议实际上在你的编译器/环境中进行测量,然后找出原因。

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