如何将长双打与 qsort 以及关于 NaN 进行比较?

Posted

技术标签:

【中文标题】如何将长双打与 qsort 以及关于 NaN 进行比较?【英文标题】:How to compare long doubles with qsort and with regard to NaN? 【发布时间】:2018-06-12 15:46:08 【问题描述】:

如何比较长双打与qsort() 和关于not-a-number?

在对可能包含非数字的数组进行排序时,我想将所有 NAN 放在排序后的数组的一端。


qsort() 对比较函数施加了一些限制。

函数应返回一个小于、等于或 如果第一个参数分别被认为小于、等于或大于第二个参数,则大于零。 C11dr §7.22.5.2 3

当相同的对象...被多次传递给比较函数时,结果应相互一致。也就是说,对于qsort,他们应该在数组上定义一个total ordering,......同一个对象应该总是以相同的方式与键进行比较。 §7.22.5 4

a <= ba 不是数字或b 不是数字时,a > b 为假。所以a > b!(a <= b) 不一样,因为如果其中一个是 NaN,它们会产生相反的结果。

如果比较函数使用return (a > b) - (a < b);,如果ab 中的一个或两个为NaN,则代码将返回0。该数组无法按预期排序,它失去了总排序要求。

在使用 int isnan(real-floating x);int isfinite(real-floating x); 等分类函数时,这种类型的 long double 方面很重要。我知道isfinite( finite_long_double_more_than_DBL_MAX) 可能会返回错误。所以我担心isnan(some_long_double) 可能会做什么一些事情出乎意料。


我尝试了以下。它显然是按需要排序的。

子问题:下面的compare() 是否足以按需要进行排序?有什么推荐的简化吗?如果没有 - 如何解决? (对于这个任务,像 0.0L 和 -0.0L 这样的值可以以任何方式排序)

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <float.h>

int compare(const void *a, const void *b) 
  const long double *fa = (const long double *) a;
  const long double *fb = (const long double *) b;
  if (*fa > *fb) return 1;
  if (*fa < *fb) return -1;

  if (*fa == *fb) 
    //return -memcmp(fa, fb, sizeof *fa); if -0.0, 0.0 order important.
    return 0;
  
  // At least one of *fa or *fb is NaN
  // is *fa a non-NaN?
  if (!isnan(*fa)) return -1;
  if (!isnan(*fb)) return 1;

  // both NaN
  return 0;
  // return -memcmp(fa, fb, tbd size); if NaN order important.


int main(void) 
  long double x[] =  0.0L / 0.0, 0.0L / 0.0, 0.0, 1.0L / 0.0, -0.0, LDBL_MIN,
      LDBL_MAX, 42.0, -1.0L / 0.0, 867-5309, -0.0 ;
  x[0] = -x[0];
  printf("unsorted: ");
  size_t n = sizeof x / sizeof x[0];
  for (size_t i = 0; i < n; i++) 
    printf("%.3Le,", x[i]);
  
  printf("\nsorted: ");
  qsort(x, n, sizeof x[0], compare);
  for (size_t i = 0; i < n; i++) 
    printf("%.3Le,", x[i]);
  
  puts("");

输出

unsorted: nan,-nan,0.000e+00,inf,-0.000e+00,3.362e-4932,1.190e+4932,4.200e+01,-inf,-4.442e+03,-0.000e+00,
sorted: -inf,-4.442e+03,-0.000e+00,0.000e+00,-0.000e+00,3.362e-4932,4.200e+01,1.190e+4932,inf,nan,-nan,

如果我知道比较函数是正确的,我会在 Code Review 上发布改进想法。然而,我对代码与那些讨厌的 NaN 正常工作的信心不足。

【问题讨论】:

检查 NaN-ness 的数字;如果一个是 NaN 而另一个不是,则报告 NaN 更小(或更大,取决于排序方向和您希望 NaN 出现的位置)。据推测,如果它们都是 NaN,则返回 0。否则,它们都不是 NaN,您可以通过适当的比较来处理它们。如果您认为 NaN 中有不同的值,那么您必须对 NaN 进行表征并根据您选择的规则进行有效比较。请注意,对 NaN 的检查可能应该先进行,而不是最后。 @MichaelDorgan:如果您认为两个“假定相等”的数字“不能表示相同”,我认为您要么是错误的,要么是错误的陈述。 @EOF 对于此代码,2 个不同的 NaN 可以比较相等 - 它们只会以某种顺序出现在排序列表中。返回 0 不违反 §7.22.5 4. @chux:我会使用isnan(),但我的要求并没有那么严格,以至于想要识别不同类型的 NaN。我知道信令和非信令 NaN;我相信有许多代表 NaN 的位模式。但我从来不需要足够详细地研究它们来了解isnan() 表面下的内容。关键的一项是,如果数组中有两个元素——比如x[10]x[30]——那么比较x[10]x[30] 应该会产生与比较x[30]x[10] 一致的结果。如果一个是负数,另一个必须是正数,或者两者都为零。 为什么不在排序前从数组中删除 NaN?即使您可以在存在 NaN 的情况下进行排序,任何后续代码都需要以某种方式处理它们的存在 - 选项只不过是忽略、丢弃或抱怨。删除 NaN 允许后续代码假设它们不存在,即更少需要检查。 【参考方案1】:

这只是对测试的简单重新排序,但如果您愿意,它会使NaN 的状态更加清晰。

int compare(const void *a, const void *b)

    const long double fa = *(const long double *) a;
    const long double fb = *(const long double *) b;

    if (isnan(fa))
    
        if (isnan(fb))
        
            return 0;
        
        return 1;
    
    if (isnan(fb))
    
        return -1;
    
    if (fa > fb) return 1;
    if (fa < fb) return -1;

    /* no more comparisons needed */
    return 0;

由于NaN 的测试位于顶部并且不应通过任何 NaN,因此可以安全地将底部三行替换为您的

return (a > b) - (a < b);

除了讨论NaN 的不同类型(听起来有点像多少天使可以在 CPU 内核上跳舞),这应该足够稳定以满足您的目的,我看不出此代码有任何可能的问题。

使用 Clang,-ffast-math-fdenormal-fp-math=[ieee|preserve-sign|positive-zero] 都不会产生其他结果。 gcc 也没有-ffast-math-funsafe-math-optimizations,甚至是-ffinite-math-only(后者很可能是因为除了与NaN 的直接比较之外没有其他操作)。

为了完整起见,我还测试了 std::numeric_limits&lt;double&gt;::signaling_NaN();std::numeric_limits&lt;double&gt;::quiet_NaN();(来自 C++ &lt;limits.h&gt;)——同样,排序顺序没有区别。

【讨论】:

这可能会使 NaN 的状态更加清晰,但您认为这与原始 compare()- 有什么功能差异(如果有)?对于 NaN 很少见的典型数据集,首先测试 NaN 显得较慢。那么唯一的优势是代码清晰吗? @chux:你在质疑你的日常安排是否正确。此代码使用另一种测试顺序,这可能导致不同的结果。因为它没有,它似乎回答了你的问题“[我的]compare() 足够了..”(用“是”?)。因此,没有什么可修复的,并且几乎没有简化的空间。 (我认为重新排序测试因此更具可读性,但没有副作用,我认为没有或只有很小的计算优势。) 首先进行isNaN 测试:这是为了消除关于可能未定义行为的任​​何挥之不去的疑虑。这样,NaN 将首先被淘汰。一个小提示:这些测试中的每一个都被编译为单个 fucomi 指令。不知道这是否会导致显着放缓(流水线等等——真的,不知道)。 这是好的答案和comment 的力量导致接受这个答案。 @chux:干杯,伙计!我对回答您的高度技术问题有些怀疑,尤其是在关注 cmets 之后(值得保留)。并感谢您对我的回答提出质疑,很高兴!【参考方案2】:

NaN 测试

int isnan(real-floating x);

isnan 宏确定其参数值是否为 NaN。首先,以比其语义类型更宽的格式表示的参数被转换为其语义类型。然后根据参数的类型确定。235235 对于 isnan 宏,确定的类型无关紧要,除非实现支持 NaN评估类型,但不在语义类型中。

isnan(some_long_double) 将按预期工作,但在稀有平台上除外。

int isunordered(real-floating x, real-floating y) 的行为与 isnan() 相似,但它可以解释这两个参数。

许多平台上,代码可以使用(a == a) 作为候选NaN 测试,因为当a 为NaN 时,其评估结果为0,否则为1。不幸的是,除非一个实现定义了__STDC_IEC_559__,否则这并不确定。


比较&gt;=, &gt;, &lt;, &lt;=和C11 7.12.14比较宏

当至少一个操作数是 NaN 时使用 &gt;=, &gt;, &lt;, &lt;= 会导致“无效”浮点异常。因此NaN 的先前测试是谨慎的,正如@usr2564301 所回答的那样

C 提供宏 isgreaterequal(), isgreaterequal(), isless(), islessthna() 进行比较并且不会引发“无效”浮点 例外。这对于double 来说是一个不错的选择,但宏使用可能与long double 不同的real-floatingisgreater(long_double_a, long_double_a) 可能评估为 double 并且不提供所需的比较结果。

分类宏的挑战在于语义类型可能比long double窄。


以下使用上述想法,并且当我阅读 C 规范时,除了罕见的情况外,所有情况下都定义明确且功能正确:当 long double 具有 NaN 但不具有 real-floating (通常double) 不会。

#include <math.h>

// compare 2 long double.  All NaN are greater than numbers.
int compare(const void *a, const void *b) 
  const long double *fa = (const long double *) a;
  const long double *fb = (const long double *) b;

  if (!isunordered(*fa, *fb)) 
    return (*fa > *fb) - (*fa < *fb);
  

  if (!isnan(*fa)) 
    return -1;
  
  return isnan(*fb);  // return 0 or 1


注意:在阅读了许多优秀的 cmets 并学到了很多东西之后,除了接受另一个答案之外,我还发布了 Can I answer my own question? 中提供的这个自我答案。

【讨论】:

以上是关于如何将长双打与 qsort 以及关于 NaN 进行比较?的主要内容,如果未能解决你的问题,请参考以下文章

HSQLDB 语句和 Java NaN 双打

对数组进行部分排序 C

回调函数和如何使用qsort函数以及最后如何运用冒泡排序完成一个各类型数据都适用的排序算法

如何在c中使用qsort比较C++字符串?

将长值列表转换为逗号分隔以及每个值周围的括号

Python - 将长/整数值与==进行比较并且是[重复]