C++中min和max函数的使用

87

在C++中,std::minstd::max函数是否比fminfmax更好?对于比较两个整数,它们提供基本相同的功能吗?

您倾向使用其中一个函数集吗,还是更喜欢编写自己的代码(也许是为了提高效率,可移植性,灵活性等)?

注:

  1. C ++标准模板库(STL)在标准C ++ algorithm 头文件中声明了 min max 函数。

  2. C标准(C99)在标准C math.h 头文件中提供了 fmin fmax 函数。

14个回答

123

fminfmax专门用于浮点数(因此有"f")。如果您对整数使用它,可能会因为转换、函数调用开销等原因而导致性能或精度损失,这取决于您的编译器/平台。

std::minstd::max是模板函数(定义在头文件<algorithm>中),可以处理具有小于 (<) 运算符的任何类型,因此可以处理允许这样比较的任何数据类型。如果不希望使用 < 进行比较,还可以提供自己的比较函数。

这样更安全,因为必须显式转换参数以匹配不同类型的参数。例如,编译器不会让您意外地将64位整数转换为64位浮点数。仅出于这个原因,模板应该成为您的默认选择。(感谢Matthieu M & bk1e)

即使在处理浮点数时,模板可能也会在性能上胜出。由于源代码是编译单元的一部分,编译器始终可以选择将调用模板函数的操作内联。在另一方面(共享库、缺乏链接时优化等)有时可能无法将调用库函数的操作内联。


9
警告:min和max只能比较完全相同类型的两个变量...因此你不能使用它们来比较int和double类型的变量 :( - Matthieu M.
6
True - max(1, 2.0) 不起作用,需要类似于 max<double>(1, 2.0) 或 max(double(1), 2.0) 这样的形式。 - David Thornley
55
在我看来,这是一件好事™ :) - Cogwheel
2
这是一个很大的假设,即转换会有成本。在某些系统上,唯一的区别可能只是在比较之前将值加载到浮点寄存器而不是普通寄存器中。 - Martin York
1
有没有支持64位整数(ILP64)和64位双精度浮点数的平台?在这些平台上,从整数转换为双精度浮点数会导致极大正/负整数的精度损失。 - bk1e
显示剩余3条评论

53

std::minstd::max以及fminfmax之间有一个重要区别。

std::min(-0.0,0.0) = -0.0
std::max(-0.0,0.0) = -0.0

然而

fmin(-0.0, 0.0) = -0.0
fmax(-0.0, 0.0) =  0.0

所以std::min并不完全等同于fmin。函数std::minstd::max是不可交换的。要在双精度浮点数上使用fminfmax得到相同的结果,应该交换参数。

fmin(-0.0, 0.0) = std::min(-0.0,  0.0)
fmax(-0.0, 0.0) = std::max( 0.0, -0.0)
但据我所知,针对这种情况所有这些函数无论如何都是实现定义的,因此为了百分之百确定,您需要测试它们的实现方式。
对于x!= NaN,还有另一个重要的区别。
std::max(Nan,x) = NaN
std::max(x,NaN) = x
std::min(Nan,x) = NaN
std::min(x,NaN) = x

然而

fmax(Nan,x) = x
fmax(x,NaN) = x
fmin(Nan,x) = x
fmin(x,NaN) = x

fmax 可以通过以下代码模拟。

double myfmax(double x, double y)
{
   // z > nan for z != nan is required by C the standard
   int xnan = isnan(x), ynan = isnan(y);
   if(xnan || ynan) {
        if(xnan && !ynan) return y;
        if(!xnan && ynan) return x;
        return x;
   }
   // +0 > -0 is preferred by C the standard 
   if(x==0 && y==0) {
       int xs = signbit(x), ys = signbit(y);
       if(xs && !ys) return y;
       if(!xs && ys) return x;
       return x;
   }
   return std::max(x,y);
}

这表明 std::maxfmax 的一个子集。

查看汇编代码可以发现,Clang 使用内置代码来处理 fmaxfmin,而 GCC 则从数学库中调用它们。使用 -O3 选项时,Clang 对于 fmax 的汇编代码如下:

movapd  xmm2, xmm0
cmpunordsd      xmm2, xmm2
movapd  xmm3, xmm2
andpd   xmm3, xmm1
maxsd   xmm1, xmm0
andnpd  xmm2, xmm1
orpd    xmm2, xmm3
movapd  xmm0, xmm2

对于std::max(double, double),它只是

maxsd   xmm0, xmm1

然而,对于使用-Ofast的GCC和Clang,fmax变得非常简单。

maxsd   xmm0, xmm1

因此,这再次表明std::maxfmax的子集,并且当您使用较松的浮点模型时,该模型不具有nan或有符号零,则fmaxstd::max相同。显然,相同的论点适用于fminstd::min


2
@greggo,C标准规定:“理想情况下,fmax应该对零的符号敏感,例如fmax(-0.0,+0.0)将返回+0;但是,在软件实现中可能不切实际。”。因此,这不是fmin / fmax的要求,而是偏好。当我测试这些函数时,它们确实执行了所需的操作。 - Z boson
@greggo,我在我的答案中已经说过了。请看代码中的注释“// z > nan for z != nan is required by C the standard”和“// +0 > -0 is preferred by C the standard”。 - Z boson
@greggo,我测试了你的说法,即maxsd/minsd会丢弃nan,但我观察到的情况并非如此http://coliru.stacked-crooked.com/a/ca78268b6b9f5c88。这些运算符就像有符号零一样不可交换。 - Z boson
@greggo,这里有一个更好的例子,我使用了 _mm_max_sd,它展示了maxsd既不会丢失nan也不会交换。http://coliru.stacked-crooked.com/a/768f6d831e79587f - Z boson
好的,第二个并不完全是我说的。没有使用-ffast-math选项,它会将fmax内联到主函数中,并且实际上使用divsd进行0.0/0.0计算,结果为“-nan 1.000000”。使用-ffast-math选项,它会在编译时全部计算出来,结果为“nan nan”。 - greggo
显示剩余3条评论

18

你完全没有理解fmin和fmax的点。它们在C99中被引入,是为了让现代CPU可以使用它们的本地(即SSE)指令来进行浮点数最小值和最大值的运算,从而避免测试和分支(以及可能的错误预测分支)。我已经重新编写了使用std::min和std::max的代码,改用SSE内部循环中的min和max指令,速度显著提升。


1
速度提升有多大?为什么C++编译器无法检测到您何时使用std::min<double>? - Janus Troelsen
5
也许在测试时他没有打开优化选项,或者编译器试图编译一个可以在“任何地方”运行的二进制文件,因此不知道它可以使用SSE。我猜想,如果你使用gcc,并传递-O3 -march=native标志,这些差异可能会消失。 - David Stone
3
它被包含在C语言中的真正原因是因为C语言没有模板或函数重载,所以他们需要为浮点类型创建一个不同名称的函数,而不仅仅是max。 - David Stone
1
刚在g++4.8上尝试了一下:fmax、std::max<double>甚至(a>b)?a:b都映射到一个maxsd指令,而且在-O1优化级别下与-O0不同,对NaN的处理也不同... - greggo

6

std::min和std::max是模板。因此,它们可以用于提供小于运算符的各种类型,包括浮点数、双精度浮点数、长双精度浮点数等。因此,如果你想编写通用的C++代码,你需要这样做:

template<typename T>
T const& max3(T const& a, T const& b, T const& c)
{
   using std::max;
   return max(max(a,b),c); // non-qualified max allows ADL
}

关于性能,我认为fminfmax与它们的C++对应函数没有区别。


1
什么是ADL,为什么我们想要它在这里? - Rob Kennedy
1
ADL = 参数相关查找。在这种情况下,它可能并不必要,因为每个带有自己max函数的用户定义类型很可能也会提供一个特殊的小于运算符。我会习惯性地编写这样的代码--主要是使用swap和一些数值函数,如abs。如果存在特殊的交换和绝对值函数,则应该使用类型的特殊函数而不是通用函数。我建议阅读Herb Sutter的关于“命名空间和接口原则”的文章:http://www.gotw.ca/publications/mill08.htm - sellibitze

6
如果你的实现提供了64位整数类型,那么使用fmin或fmax会得到不同(错误的)答案。你的64位整数将被转换为双精度浮点数,其有效数字(至少通常情况下)小于64位。当你将这样的数字转换为双精度浮点数时,一些最不重要的位数可能会完全丢失。
这意味着两个本来不同的数字在转换为双精度浮点数后可能相等——结果将是错误的数字,不一定等于任何一个原始输入。

4
如果你在使用C++,我建议你使用C++的min/max函数,因为它们是类型特定的。fmin/fmax会强制所有内容转换为浮点数。
另外,只要你已经为自定义类型定义了operator<,C++的min/max函数也适用于自定义类型。
希望这对你有所帮助。

3

正如Richard Corden所指出的,使用std命名空间中定义的C++函数min和max。它们提供类型安全,并帮助避免比较混合类型(例如浮点数与整数),有时可能是不可取的。

如果您发现您使用的C++库也将min/max定义为宏,这可能会引起冲突,那么可以通过以下方式调用min/max函数来防止不必要的宏替换(请注意额外的括号):

(std::min)(x, y)
(std::max)(x, y)

请记住,这将有效地禁用参数依赖查找(ADL,也称为Koenig查找),以防您想要依赖ADL。


2

2
正如您所指出的,fminfmax是在C99中引入的。标准C++库没有fminfmax函数。直到C99标准库被纳入C++(如果有的话),这些函数的应用领域将得到清晰的区分。不存在必须“优先”使用其中一个函数的情况。
在C++中,您只需使用模板化的std::min/std::max,并在C中使用可用的函数即可。

1

使用 std::minstd::max

如果其他版本更快,那么您的实现可以添加这些重载,从而获得性能和可移植性的好处:

template <typename T>
T min (T, T) {
  // ... default
}

inline float min (float f1, float f2) {
 return fmin( f1, f2);
}    

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