• Taking hypot(3) To The Edge

    From Lawrence =?iso-8859-13?q?D=FFOliveiro?=@ldo@nz.invalid to comp.lang.c on Thu Oct 1 00:47:37 2026
    From Newsgroup: comp.lang.c

    Some discussion in another thread prompted me to wonder exactly how
    close to the limit a function like hypot(3) can be taken and still
    give meaningful results. So I wrote a test script which generates a C
    program using different real types, runs it, and reports the results:

    #!/usr/bin/python3
    #+
    # Probe the usable limits of the hypot(3) function.
    #-

    import sys
    import os
    import tempfile
    import subprocess
    import shutil

    src_template = \
    """#include <math.h>
    #include <values.h>
    #include <stdio.h>

    int main(void)
    {
    fprintf(stdout, "sizeof(%(realtype)s) = %%d\\n", sizeof(%(realtype)s));
    fprintf(stdout, "%(maxreal)s (%(realtype)s) = %%.%(prec)se\\n", %(maxreal)s);
    const %(realtype)s down1 = %(nextafter)s(%(maxreal)s, -1.0);
    fprintf(stdout, "next towards 0 = %%.%(prec)se\\n", down1);
    fprintf(stdout, "nr sig bits = %%ld\\n", lrint(floor(- %(log2)s((%(maxreal)s - down1) / down1))));
    fprintf(stdout, "nr sig digits = %%ld\\n", lrint(floor(- %(log10)s((%(maxreal)s - down1) / down1))));
    const %(realtype)s factor = %(sqrt)s(2.0);
    const %(realtype)s factor_next = %(nextafter)s(factor, INFINITY);
    const %(realtype)s factor_prev = %(nextafter)s(factor, - INFINITY);
    fprintf(stdout, "hypot(%%.%(prec)se) = %%.%(prec)se\\n", %(maxreal)s / factor, %(hypot)s(%(maxreal)s / factor, %(maxreal)s / factor));
    fprintf(stdout, "hypot(%%.%(prec)se) = %%.%(prec)se\\n", %(maxreal)s / factor_next, %(hypot)s(%(maxreal)s / factor_next, %(maxreal)s / factor_next));
    fprintf(stdout, "hypot(%%.%(prec)se) = %%.%(prec)se\\n", %(maxreal)s / factor_prev, %(hypot)s(%(maxreal)s / factor_prev, %(maxreal)s / factor_prev));
    return
    0;
    } /*main*/
    """

    tempdir = tempfile.mkdtemp(prefix = "hypot_test")
    progsrc = os.path.join(tempdir, "test.c")
    progbin = os.path.join(tempdir, "test")

    for parms in \
    (
    ("float", "FLT_MAX", "7", "nextafterf",
    "log2f", "log10f", "sqrtf", "hypotf"),
    ("double", "DBL_MAX", "16", "nextafter",
    "log2", "log10", "sqrt", "hypot"),
    ("long double", "LDBL_MAX", "20L", "nextafterl",
    "log2l", "log10l", "sqrtl", "hypotl"),
    ) \
    :
    src = open(progsrc, "wt")
    src.write \
    (
    src_template
    %
    dict
    (
    zip
    (
    ("realtype", "maxreal", "prec", "nextafter",
    "log2", "log10", "sqrt", "hypot"),
    parms
    )
    )
    )
    src.close()
    subprocess.check_call(args = ("gcc", "-o", progbin, progsrc, "-lm"))
    subprocess.check_call(args = (progbin,))
    sys.stdout.write("\n")
    #end for

    shutil.rmtree(tempdir)

    And hererCOs what I got:

    ldo@theon:python_try> ./c_hypot_test
    sizeof(float) = 4
    FLT_MAX (float) = 3.4028235e+38
    next towards 0 = 3.4028233e+38
    nr sig bits = 24
    nr sig digits = 7
    hypot(2.4061597e+38) = inf
    hypot(2.4061594e+38) = 3.4028233e+38
    hypot(2.4061599e+38) = inf

    sizeof(double) = 8
    DBL_MAX (double) = 1.7976931348623157e+308
    next towards 0 = 1.7976931348623155e+308
    nr sig bits = 53
    nr sig digits = 15
    hypot(1.2711610061536460e+308) = 1.7976931348623155e+308
    hypot(1.2711610061536458e+308) = 1.7976931348623151e+308
    hypot(1.2711610061536462e+308) = 1.7976931348623157e+308

    sizeof(long double) = 16
    LDBL_MAX (long double) = 1.18973149535723176502e+4932
    next towards 0 = 1.18973149535723176496e+4932
    nr sig bits = 64
    nr sig digits = 19
    hypot(8.41267208158310063619e+4931) = inf
    hypot(8.41267208158310063554e+4931) = 1.18973149535723176496e+4932
    hypot(8.41267208158310063683e+4931) = inf

    A couple of things stand out immediately: one is that the rCLlong
    doublerCY type doesnrCOt seem to make use of all the 128 bits it occupies.
    I was expecting a mantissa length closer to 100 bits, but itrCOs nowhere
    near that.

    Another is that the rounding errors lead to some interesting precision-dependent edge behaviour. double, in particular, seems to be
    able to produce a result that is not rCLinfrCY a little bit beyond the
    limit that you would expect.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Steven G. Kargl@sgk@REMOVEtroutmask.apl.washington.edu to comp.lang.c on Thu Oct 1 01:02:32 2026
    From Newsgroup: comp.lang.c

    On Thu, 1 Oct 2026 00:47:37 -0000 (UTC), Lawrence DrCOOliveiro wrote:

    Some discussion in another thread prompted me to wonder exactly how
    close to the limit a function like hypot(3) can be taken and still
    give meaningful results. So I wrote a test script which generates a C
    program using different real types, runs it, and reports the results:



    python? This is a newgroup about C, isn't?

    hypot(2.4061597e+38) = inf
    hypot(2.4061594e+38) = 3.4028233e+38
    hypot(2.4061599e+38) = inf

    hypot() takes two arguments. Are we to guess what the 2nd argement is?


    sizeof(long double) = 16
    LDBL_MAX (long double) = 1.18973149535723176502e+4932
    next towards 0 = 1.18973149535723176496e+4932
    nr sig bits = 64
    ^^^^^^^^^^^^^

    nr sig digits = 19
    hypot(8.41267208158310063619e+4931) = inf
    hypot(8.41267208158310063554e+4931) = 1.18973149535723176496e+4932
    hypot(8.41267208158310063683e+4931) = inf

    A couple of things stand out immediately: one is that the rCLlong
    doublerCY type doesnrCOt seem to make use of all the 128 bits it occupies.
    I was expecting a mantissa length closer to 100 bits, but itrCOs nowhere
    near that.

    Why would long double have a significand with nominally 100 bits, when
    the precision is 64 bits? Are you using an Intel/AMD cpu by chance?

    PS: Instead of using python and trying to guess model numbers, you
    could available yourself of float.h.

    #define LDBL_MANT_DIG 64
    #define LDBL_EPSILON 1.0842021724855044340E-19L
    #define LDBL_DIG 18
    #define LDBL_MIN_EXP (-16381)
    #define LDBL_MIN 3.3621031431120935063E-4932L
    #define LDBL_MIN_10_EXP (-4931)
    #define LDBL_MAX_EXP 16384
    #define LDBL_MAX 1.1897314953572317650E+4932L
    #define LDBL_MAX_10_EXP 4932
    #if __ISO_C_VISIBLE >= 2011
    #define LDBL_TRUE_MIN 3.6451995318824746025E-4951L
    #define LDBL_DECIMAL_DIG 21
    #define LDBL_HAS_SUBNORM 1
    #endif /* __ISO_C_VISIBLE >= 2011 */
    --
    steve

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From bart@bc@freeuk.com to comp.lang.c on Thu Oct 1 02:33:01 2026
    From Newsgroup: comp.lang.c

    On 01/10/2026 01:47, Lawrence DrCOOliveiro wrote:

    A couple of things stand out immediately: one is that the rCLlong
    doublerCY type doesnrCOt seem to make use of all the 128 bits it occupies.
    I was expecting a mantissa length closer to 100 bits, but itrCOs nowhere
    near that.

    Probably it uses Intel's 80-bit x87 FPU format. That uses a 64-bit
    mantissa (with explicit top bit).

    10 bytes would be an odd size though, so it's rounded up to 16 bytes in
    the implementation.

    I don't know if current hardware would directly support a full 128 bits,
    or it needs to be emulated.

    However, float limits (in float.h) should give a clue as to the actual
    range and precision.


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Lawrence =?iso-8859-13?q?D=FFOliveiro?=@ldo@nz.invalid to comp.lang.c on Thu Oct 1 01:55:43 2026
    From Newsgroup: comp.lang.c

    On Thu, 1 Oct 2026 01:02:32 -0000 (UTC), Steven G. Kargl wrote:

    Why would long double have a significand with nominally 100 bits ...

    It takes up 128 bits. The exponent only seems to be about 15 bits
    (-#4931-ish in base-10). So what else is using that space?
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Lawrence =?iso-8859-13?q?D=FFOliveiro?=@ldo@nz.invalid to comp.lang.c on Thu Oct 1 02:38:47 2026
    From Newsgroup: comp.lang.c

    On Thu, 1 Oct 2026 02:33:01 +0100, bart wrote:

    Probably it uses Intel's 80-bit x87 FPU format. That uses a 64-bit
    mantissa (with explicit top bit).

    80 bits sounds about right.

    10 bytes would be an odd size though, so it's rounded up to 16 bytes
    in the implementation.

    On the old Apple Mac, this was the rCLextendedrCY format, which occupied
    10 bytes. MotorolarCOs 68881 and 68882 FPU chips used 12 bytes (96
    bits), with two padding bytes just for alignmentrCOs sake.

    I don't know if current hardware would directly support a full 128
    bits, or it needs to be emulated.

    I thought some systems had a software-implemented format consisting of
    two doubles, to combine the accuracy of their two mantissas somehow.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Steven G. Kargl@sgk@REMOVEtroutmask.apl.washington.edu to comp.lang.c on Thu Oct 1 03:44:59 2026
    From Newsgroup: comp.lang.c

    On Thu, 1 Oct 2026 01:55:43 -0000 (UTC), Lawrence DrCOOliveiro wrote:

    On Thu, 1 Oct 2026 01:02:32 -0000 (UTC), Steven G. Kargl wrote:

    Why would long double have a significand with nominally 100 bits ...

    It takes up 128 bits. The exponent only seems to be about 15 bits
    (-#4931-ish in base-10). So what else is using that space?

    Well, if you're using an Intel/AMD cpu, it is mostlikely junk.

    https://www.uclibc.org/docs/psABI-x86_64.pdf

    See page 11.
    --
    steve

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@david.brown@hesbynett.no to comp.lang.c on Thu Oct 1 10:17:57 2026
    From Newsgroup: comp.lang.c

    On 01/10/2026 03:33, bart wrote:
    On 01/10/2026 01:47, Lawrence DrCOOliveiro wrote:

    A couple of things stand out immediately: one is that the rCLlong
    doublerCY type doesnrCOt seem to make use of all the 128 bits it occupies. >> I was expecting a mantissa length closer to 100 bits, but itrCOs nowhere
    near that.

    Probably it uses Intel's 80-bit x87 FPU format. That uses a 64-bit
    mantissa (with explicit top bit).


    Yes, that's the default for "long double" in the standard x86-64 ABI.

    On 32-bit x86, these 10-byte doubles were often stored in 12-byte
    (96-bit) containers for better alignment, but I don't know the ABI
    standards here.

    On 64-bit x86, for better alignment they are stored in 16-byte
    containers. The rest of the space will be padding.

    gcc supports "-mlong-double-64", "-mlong-double-80" and
    "-mlong-double-128" flags. With 64-bit long doubles, they are
    effectively the same as doubles, and thus faster. 80-bit long doubles
    are the standard, while 128-bit long doubles give quad-precision range
    and precision, but must be handled in software through the gcc-provided "libquad" library functions.

    You can also use the _Float128 or _Float128x types, which are optional
    types in the C standards (along with 16, 32 and 64-bit versions). The
    "x" types are for calculations and might be bigger than their name
    applies, while the non-x types are "interchange" types that have the
    same format on all systems.


    10 bytes would be an odd size though, so it's rounded up to 16 bytes in
    the implementation.

    I don't know if current hardware would directly support a full 128 bits,
    or it needs to be emulated.


    IBM's POWER processors are, I think, the only cpus with hardware quad-precision floating point that are realistic. RISC-V has the Q
    extensions specified, but I do not believe anyone actually makes RISC-V
    cores with quad floating point.

    However, float limits (in float.h) should give a clue as to the actual
    range and precision.


    Yes.

    You can also just write some code in godbolt.org, and see if it calls
    library functions like "__multf3" instead of generating hardware
    instructions, as you play around with the compiler flags.


    --- Synchronet 3.22a-Linux NewsLink 1.2