【问题标题】:Implement ceil() in C在 C 中实现 ceil()
【发布时间】:2012-09-05 10:57:12
【问题描述】:

我想在C 中实现我自己的ceil()。在库中搜索源代码并找到here,但似乎很难理解。我想要干净优雅的代码。

我也在SO上搜索,找到了一些答案here。似乎没有一个答案是正确的。答案之一是:

#define CEILING_POS(X) ((X-(int)(X)) > 0 ? (int)(X+1) : (int)(X))
#define CEILING_NEG(X) ((X-(int)(X)) < 0 ? (int)(X-1) : (int)(X))
#define CEILING(X) ( ((X) > 0) ? CEILING_POS(X) : CEILING_NEG(X) )

AFAIK,ceil() 的返回类型不是 int。宏在这里是类型安全的吗? 此外,上述实现是否适用于负数?

实现它的最佳方式是什么?

你能提供干净的代码吗?

【问题讨论】:

  • 宏通常从不类型安全。它们还容易产生意想不到的副作用(例如,将x++ 作为参数传递可能会根据宏多次增加x)。
  • 它可能由内置编译器实现,或者由一些汇编例程实现。如果不仔细了解编译器内部结构、处理器架构和 IEEE 表示,我不会尝试重新实现 ceil
  • 我理解 x+x 用于引发溢出或 nan,但不明白为什么参考代码要引发不精确标志 (huge+x)?如果 x 是整数,那么 ceil(x) 就是 x,如果 x 有小数部分,那么 ceil(x) 也是众所周知的并且也可以精确表示,还是我错过了什么?
  • 糟糕,忘记阅读注释“如果 x 不等于 ceil(x),则会引发不精确标志。”,因为所有舍入函数都是精确的,所以这种用法是有意义的,并且可能由某些标准规定。

标签: c floating-point implementation ceil


【解决方案1】:

您引用的宏对于大于INT_MAX 但仍可以精确表示为双精度的数字肯定无法正常工作。

正确实现ceil() 的唯一方法(假设您不能使用等效的汇编指令实现它)是对浮点数的二进制表示进行位旋转,就像在s_ceil.c 中所做的那样第一个链接后面的源文件。理解代码是如何工作的需要理解底层平台的浮点表示——表示很可能是IEEE 754——但没有办法解决这个问题。

编辑:

s_ceil.c 中的一些复杂性源于它处理的特殊情况(NaN、无穷大)以及它需要在无法假设存在 64 位整数类型的情况下完成其工作。

所有位旋转的基本思想是屏蔽尾数的小数位,如果数字大于零,则在其上加 1...但是还涉及一些额外的逻辑以确保在所有情况下你都做正确的事。

这是我拼凑在一起的 ceil() 的说明性版本。当心:这不能正确处理特殊情况,并且没有经过广泛测试——所以不要实际使用它。然而,它确实有助于说明比特旋转所涉及的原理。我试图广泛地评论该例程,但 cmets 确实假设您了解浮点数在 IEEE 754 格式中的表示方式。

union float_int
{
    float f;
    int i;
};

float myceil(float x)
{
    float_int val;
    val.f=x;

    // Extract sign, exponent and mantissa
    // Bias is removed from exponent
    int sign=val.i >> 31;
    int exponent=((val.i & 0x7fffffff) >> 23) - 127;
    int mantissa=val.i & 0x7fffff;

    // Is the exponent less than zero?
    if(exponent<0) 
    {   
        // In this case, x is in the open interval (-1, 1)
        if(x<=0.0f)
            return 0.0f;
        else
            return 1.0f;
    }
    else
    {
        // Construct a bit mask that will mask off the
        // fractional part of the mantissa
        int mask=0x7fffff >> exponent;

        // Is x already an integer (i.e. are all the
        // fractional bits zero?)
        if((mantissa & mask) == 0)
            return x;
        else
        {
            // If x is positive, we need to add 1 to it
            // before clearing the fractional bits
            if(!sign)
            {
                mantissa+=1 << (23-exponent);

                // Did the mantissa overflow?
                if(mantissa & 0x800000)
                {
                    // The mantissa can only overflow if all the
                    // integer bits were previously 1 -- so we can
                    // just clear out the mantissa and increment
                    // the exponent
                    mantissa=0;
                    exponent++;
                }
            }

            // Clear the fractional bits
            mantissa&=~mask;
        }
    }

    // Put sign, exponent and mantissa together again
    val.i=(sign << 31) | ((exponent+127) << 23) | mantissa;

    return val.f;
}

【讨论】:

  • 确定吗?如果 x = 2^23-0.5... exponent=22, mantissa=0x7fffff, 你添加 1
  • @aka.nice 很好,您的修复似乎确实有效。我想这就是我说“小心”的原因......
  • 是的,如果是单个错误,考虑到圈复杂度,它已经是一个不错的分数了;)我也刚试过,你必须声明 union float_int val;在纯 C 中。
  • @aka.nice 猜猜我说 C 时带有 C++ 口音... ;) 我对这个 bug 有了更多的思考。它实际上可能发生在比 2^23-0.5 小得多的数字上——本质上,只要尾数溢出,错误就会发生。例如,7.01f 将触发该错误。我已经编辑了代码,希望能解决这个问题——你提出的修复应该可以工作,但为了清楚起见,我选择显式处理尾数溢出。请注意,我已经能够摆脱exponent==0 的情况(这本质上只是尾数溢出的一种特殊情况)。再次感谢指点。
  • 各种注意事项:如果要这样做,最好分离出组件字段;这样,有效数字就会溢出到指数中,无论如何这就是你想要发生的事情。如果指数很大,则需要添加早出;否则你的班次变得不确定。还有一个可爱的技巧:您可以直接使用significand = significand + mask &amp; ~mask,而不是将 1 移到正确的位置,这通常更有效。最后,对于小的负输入,您会得到错误的零符号。
【解决方案2】:

没有什么比使用标准库实现更优雅了。没有任何代码总是比优雅的代码更优雅。

除此之外,这种方法有两个主要缺陷:

  • 如果X 大于INT_MAX + 1 或小于INT_MIN - 1,则宏的行为未定义。这意味着您的实现可能对所有浮点数的近一半给出不正确的结果。与 IEEE-754 相反,您还将引发无效标志。
  • 它得到了 -0、+/-infinity 和 nan 错误的边缘情况。事实上,它唯一正确的边缘情况是 +0。

可以以类似于您尝试的方式实现 ceil,就像这样(此实现假定 IEEE-754 双精度):

#include <math.h>

double ceil(double x) {
    // All floating-point numbers larger than 2^52 are exact integers, so we
    // simply return x for those inputs.  We also handle ceil(nan) = nan here.
    if (isnan(x) || fabs(x) >= 0x1.0p52) return x;
    // Now we know that |x| < 2^52, and therefore we can use conversion to
    // long long to force truncation of x without risking undefined behavior.
    const double truncation = (long long)x;
    // If the truncation of x is smaller than x, then it is one less than the
    // desired result.  If it is greater than or equal to x, it is the result.
    // Adding one cannot produce a rounding error because `truncation` is an
    // integer smaller than 2^52.
    const double ceiling = truncation + (truncation < x);
    // Finally, we need to patch up one more thing; the standard specifies that
    // ceil(-small) be -0.0, whereas we will have 0.0 right now.  To handle this
    // correctly, we apply the sign of x to the result.
    return copysign(ceiling, x);
}

这样的事情几乎是你能得到的优雅并且仍然是正确的。


我对 Martin 在他的回答中提出的(通常很好!)实现提出了一些担忧。以下是我将如何实施他的方法:

#include <stdint.h>
#include <string.h>

static inline uint64_t toRep(double x) {
    uint64_t r;
    memcpy(&r, &x, sizeof x);
    return r;
}

static inline double fromRep(uint64_t r) {
    double x;
    memcpy(&x, &r, sizeof x);
    return x;
}

double ceil(double x) {

    const uint64_t signbitMask  = UINT64_C(0x8000000000000000);
    const uint64_t significandMask = UINT64_C(0x000fffffffffffff);

    const uint64_t xrep = toRep(x);
    const uint64_t xabs = xrep & signbitMask;

    // If |x| is larger than 2^52 or x is NaN, the result is just x.
    if (xabs >= toRep(0x1.0p52)) return x;

    if (xabs < toRep(1.0)) {
        // If x is in (1.0, 0.0], the result is copysign(0.0, x).
        // We can generate this value by clearing everything except the signbit.
        if (x <= 0.0) return fromRep(xrep & signbitMask);
        // Otherwise x is in (0.0, 1.0), and the result is 1.0.
        else return 1.0;
    }

    // Now we know that the exponent of x is strictly in the range [0, 51],
    // which means that x contains both integral and fractional bits.  We
    // generate a mask covering the fractional bits.
    const int exponent = xabs >> 52;
    const uint64_t fractionalBits = significandMask >> exponent;

    // If x is negative, we want to truncate, so we simply mask off the
    // fractional bits.
    if (xrep & signbitMask) return fromRep(xrep & ~fractionalBits);

    // x is positive; to force rounding to go away from zero, we first *add*
    // the fractionalBits to x, then truncate the result.  The add may
    // overflow the significand into the exponent, but this produces the
    // desired result (zero significand, incremented exponent), so we just
    // let it happen.
    return fromRep(xrep + fractionalBits & ~fractionalBits);
}

关于这种方法需要注意的一点是,它确实不会为非整数输入引发不精确的浮点标志。这可能会或可能不会影响您的使用。我列出的第一个实现确实会引起注意。

【讨论】:

  • +1 表示“没有代码总是比优雅的代码更优雅”。
【解决方案3】:

我不认为宏函数是一个好的解决方案:它不是类型安全的,并且存在对参数的多重评估(副作用)。你应该写一个干净优雅的函数

【讨论】:

  • 你能提供干净优雅的代码吗?我试过了,但在某些情况下它不起作用?
  • 好吧,你应该检查你的实现细节(另请参见 FredOverflow 的 IEEE754 答案)。 stackoverflow.com/questions/8377412/…
【解决方案4】:

正如我所期望的答案中的更多笑话,我会尝试几个

#define CEILING(X) ceil(X)

奖励:一个没有太多副作用的宏
如果你不太在意负零

#define CEILING(X) (-floor(-(X)))

如果你关心负零,那么

#define CEILING(X) (NEGATIVE_ZERO - floor(-(X)))

NEGATIVE_ZERO 的便携定义作为练习.... 奖励,它还会设置 FP 标志(OVERFLOW INVALID INEXACT)

【讨论】:

  • 我本来预计会有更多的 cmets 在这个上!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-12
  • 2022-11-20
  • 1970-01-01
  • 2021-12-26
  • 1970-01-01
相关资源
最近更新 更多