libm

Windows10に早くしろしろウィルスに感染したWindows7機、約5時間待ってみたけど、 さっっぱり更新が始まらない。観念して個別にインストールしようと思って、 パッチをDLし始めた。

DLスピードはめちゃ遅い。60kbpsぐらいしか出ない。サーバー側は未だにADSL端子に なってるんではなかろうか? それとも7ユーザーに嫌がらせの意味を込めて、 優先度を最低ランクの端子にしてないか?

そんなこんなでDL中に、ふとアップデートの画面を見たら、やっとDLできまっせ案内が 来てた。開始してから5時間半ぶりですよ。そして、総DL容量は86Mですって。ここからも 忍耐の時間が始まるのね。もー、やだやだ。付き合っていられないな。

libm

前回、数学で最も美しい式と言われる、オイラーさんが見つけた式を、各種言語で やってみた。その結果、オイラーさんが(きっと)現代コンピュータ言語を嘆いて いるに違いないと思った。

虚部の項はZEROになるはずなのに、若干の値が残っている。原因はきっとlibm.so に有るだろうと推測。eのべき乗を求める関数 exp は、超越関数。実行スピードを 稼ぐ為に、あの糞石メーカー製作の i387が使われているに違いない。

ならば、どんなOSを持ってこようと、x86で動くOS上なら、同じ結果になるはず。 暇なんで、検証してみる。

検証方法は、gaucheを基本として、定数の pi は、 2 * acos(0) で、計算させる 事にした。これなら、定数の定義桁数によらない値を使ってくれると思ったから。

$ gosh
gosh> (exp (* 0+1i 2 (acos 0)))
-1.0+1.2246063538223773e-16i

実部の表示は、1 だったり、1. だったりするけど、見やすいように、1.0 とした。

FreeBSD       -1.0+1.2246063538223773e-16i  ; gauche
OpenBSD       -1.0+1.2246063538223773e-16i  ; gambit
NetBSD        -1.0+1.2246063538223773e-16j  ; python
Fedora        -1.0+1.2246467991473532e-16i  ; gauche
Ubuntsu       -1.0+1.2246467991473532e-16i  ; gauche
iOS9          -1.0+1.2246467991473532e-16i  ; gambit
iOS9          -1.0+1.22464679915e-16j       ; pythonista

何やら、BSD vs. Linux と言う構図だな。これはもうソース嫁の世界に突入必至か。 まてまて、オイラーは、ARMな石も持ってるぞ。この石なら、どう転んでも、i387は 使えまえ。iOS9にも参入して貰った。もう、ipadを使い始めて3年経過したけど、 こんな事したのは初めてだ。

iOS族もLinuxに右に倣えしてるな。アプルはlibmみたいのを共通基盤として提供してる? それはちょっと考えづらいな。libmなんてわざわざ指定しないとリンク出来ない人気の なさ。と言うか特殊なもの。

あれ、画像関係でsinとかcosが多用されるけど、どうなってるの? そんなものは、 画像データ生成・加工機のGPUに丸投げさ。この辺を90度回転してって下請けに出す だけ。だから、libmは不要と思われ。

ユーザーが使っていいのは、画面の描画とか通信とかの派手系ぐらいだろう。 用意したとしても、ゲームで多用されるであろう、乱数の発生ぐらいか。ああ、乱数は セキュリティの要になるから、絶対アプル製だろうね。

以前、appの構成をちら見したけど、appが使うライブラリィ類は、 appの中に閉じていたように思う。って事は、app作者が自前で用意するしかない。

人気はLinux系って事で、GNUのソースをコンパイルしてappに直付けしたか、ライブラリィ で持つ事にしたのだろう。だから、指紋が残ったとな。

gambitのソースは公開済み。pythonistaって言うpython系は、ソースが公開されてるのかな? 若し未公開なら、GPLに汚染してる証拠を提示して、公開を迫ってみようか。

例によってLinux系のソースは入れてないので、OpenBSDのlibmを紐解いてみますかな。

複素数演算の探検用ソース

libmを探検する前に、探検用のツールを作っておく。goshのコードをC語に置き換えた やつを書く。

#include <stdio.h>
#include <complex.h>
#include <math.h>

int main(void){
   long double _Complex z, x;

   x = I * 2 * acosl(0);
   z = cexpl(x);

   printf("real=%g\timag=%38.36g\n", creal(z),cimag(z));

   return 0;
}

complex.hを使うと、虚数単位(0+1i)を大文字のIとして使えるようになる。また、πは、M_PIとして 定義されてるので、それを利用してもいいのだけど、goshに習って自前で導出した。 全体は、long doubleで無駄に精度よく計算してみた。複素数の関数の説明は、 標準ライブラリ(新規)が詳しい。C99規格 なんですねぇ。C語も地味に進化してるな。

違うって、複素数と言えば昔からFortran。この牙城を崩すには、C陣営も複素数について 早く規格化する事が求められた。それで、21世紀になる前に、どうしても決める必要が 有ったのさ。だから、誇らしげにC99って言ってる。願わくば、21世紀はC語が覇者と たらん事を。ああ、それなのにそれなのに、Javascriptがしゃしゃり出てきおって。 Cフラフラはしぶとく、改定を重ねているな。

使い方は、

[ob: z]$ cc -g t.c -lm
[ob: z]$ ./a.out
real=-1 imag=-5.01655761266833226942395050995492469e-20

演算精度を高めたら、誤差項が小さくなったぞ。出来上がった a.outをobjdump -S して、コードを覗いてみる。

   z = cexpl(x);
 ba3:   8d 75 88                lea    0xffffff88(%ebp),%esi
 ba6:   8b 45 a8                mov    0xffffffa8(%ebp),%eax
 ba9:   8b 55 ac                mov    0xffffffac(%ebp),%edx
 bac:   8b 4d b0                mov    0xffffffb0(%ebp),%ecx
 baf:   89 44 24 04             mov    %eax,0x4(%esp)
 bb3:   89 54 24 08             mov    %edx,0x8(%esp)
 bb7:   89 4c 24 0c             mov    %ecx,0xc(%esp)
 bbb:   8b 45 b4                mov    0xffffffb4(%ebp),%eax
 bbe:   8b 55 b8                mov    0xffffffb8(%ebp),%edx
 bc1:   8b 4d bc                mov    0xffffffbc(%ebp),%ecx
 bc4:   89 44 24 10             mov    %eax,0x10(%esp)
 bc8:   89 54 24 14             mov    %edx,0x14(%esp)
 bcc:   89 4c 24 18             mov    %ecx,0x18(%esp)
 bd0:   89 34 24                mov    %esi,(%esp)
 bd3:   e8 f8 fb ff ff          call   7d0 <__init+0x50>
 bd8:   83 ec 04                sub    $0x4,%esp
 bdb:   8b 45 88                mov    0xffffff88(%ebp),%eax
 bde:   8b 55 8c                mov    0xffffff8c(%ebp),%edx
 be1:   8b 4d 90                mov    0xffffff90(%ebp),%ecx
 be4:   89 45 c8                mov    %eax,0xffffffc8(%ebp)
 be7:   89 55 cc                mov    %edx,0xffffffcc(%ebp)
 bea:   89 4d d0                mov    %ecx,0xffffffd0(%ebp)
 bed:   8b 45 94                mov    0xffffff94(%ebp),%eax
 bf0:   8b 55 98                mov    0xffffff98(%ebp),%edx
 bf3:   8b 4d 9c                mov    0xffffff9c(%ebp),%ecx
 bf6:   89 45 d4                mov    %eax,0xffffffd4(%ebp)
 bf9:   89 55 d8                mov    %edx,0xffffffd8(%ebp)
 bfc:   89 4d dc                mov    %ecx,0xffffffdc(%ebp)

盛大にmov命令が並んでいるな。longのdoubleを32Bitマシンでやると、こういう塩梅に なるのか。早く来い来い64Bitマシン。0xbd3が、関数cexplへの入り口のようだ。

ちょいとgdbで追ってみる。

Breakpoint 1, cexpl (z=0 + 3.1415926535897932385128089594061862 * I)
    at /usr/src/lib/libm/src/s_cexpl.c:66
66              r = expl(creall(z));
  :
main () at t.c:11
11         printf("real=%g\timag=%38.36g\n", creal(z),cimag(z));

もう、mainに戻ってきた。滞空時間はわずか。でも、ここ見ろって教えられたから 良しとしよう。

60long double complex
61cexpl(long double complex z)
62{
63        long double complex w;
64        long double r;
65
66        r = expl(creall(z));
67        w = r * cosl(cimagl(z)) + (r * sinl(cimagl(z))) * I;
68        return (w);
69}

もう全く数学の世界です。複素数が実世界にひょいと顔を出す。で、このdir近辺を見ると、

[ob: libm]$ ls
Makefile        man/            obj@            src/
arch/           noieee_src/     shlib_version
[ob: libm]$ ls arch/i387/
e_acos.S            e_sqrtf.S           s_floor.S           s_rint.S
e_asin.S            e_sqrtl.S           s_floorf.S          s_rintf.S
e_atan2.S           fenv.c              s_ilogb.S           s_scalbnf.S
e_atan2f.S          invtrig.c           s_ilogbf.S          s_significand.S
e_exp.S             s_atan.S            s_llrint.S          s_significandf.S
e_fmod.S            s_atanf.S           s_llrintf.S         s_sin.S
e_log.S             s_ceil.S            s_log1p.S           s_sinf.S
e_log10.S           s_ceilf.S           s_log1pf.S          s_tan.S
e_remainder.S       s_copysign.S        s_logb.S            s_tanf.S
e_remainderf.S      s_copysignf.S       s_logbf.S
e_scalb.S           s_cos.S             s_lrint.S
e_sqrt.S            s_cosf.S            s_lrintf.S

一応、i387用のコードも置いてある。ちょいと、i387/e_exp.Sを見ておくか。

/* e^x = 2^(x * log2(e)) */
ENTRY(exp)
   :
        fldl2e
        fmulp                           /* x * log2(e) */
        fst     %st(1)
        frndint                         /* int(x * log2(e)) */
        fst     %st(2)
        fsubrp                          /* fract(x * log2(e)) */
        f2xm1                           /* 2^(fract(x * log2(e))) - 1 */
        fld1
        faddp                           /* 2^(fract(x * log2(e))) */
        fscale                          /* e^x */
   :

糞石で直接expは計算出来ないから、別の方法を取るとな。これが、i387の実体。 昔はスピードを稼ぐために持て囃されたけど、アメリカの団体 IEEE が、押す ルーチンを使えやゴリャと、押し付けが有った模様。

そのせいで、i387も、反IEEEなルーチンも、過去の遺物として、ひっそりと置いて あるって事だな。米国のごり押し、恐るべしが結論ですかね。noieee_srcの中を みると、

 * Copyright (c) 1985, 1993
 *      The Regents of the University of California.  All rights reserved.

こんなクレジットが入っている。学が産業界に押されたって事か。

でも、普通に考えると、不要なソースを入れて(保持)おくはずが無い。どういう風に コンパイルされてるか調べれば良いだろう。Makefileを読むのも手だけど、擬似実行して ログを眺めるのが楽かな。

[ob: libm]$ make -n | tee ~/MLOG
echo "cc -g  -I/usr/src/lib/libm/src -I/usr/src/lib/libm/src/ld80 -c   /usr/src/lib/libm/arch/i387/e_acos.S -o e_acos.o"
cc -g  -I/usr/src/lib/libm/src -I/usr/src/lib/libm/src/ld80 -c   /usr/src/lib/libm/arch/i387/e_acos.S -o e_acos.o.o
ld -X -r e_acos.o.o -o e_acos.o
rm -f e_acos.o.o
  :
echo building standard m library
rm -f libm.a
ar cq libm.a `lorder b_exp__D.o b_log__D.o b_tgamma.o e_acos.o e_acosf.o ....
 :
echo "cc -g  -I/usr/src/lib/libm/src -I/usr/src/lib/libm/src/ld80 -c -DPROF   /usr/src/lib/libm/arch/i387/e_remainder.S  -o e_remainder.po"
cc -g  -I/usr/src/lib/libm/src -I/usr/src/lib/libm/src/ld80 -c -DPROF   /usr/src/lib/libm/arch/i387/e_remainder.S  -o e_remainder.po.o
ld -X -r e_remainder.po.o -o e_remainder.po
rm -f e_remainder.po.o
 :
echo building profiled m library
rm -f libm_p.a
ar cq libm_p.a `lorder b_exp__D.po b_log__D.po b_tgamma.po e_acos.po ...
 :
echo "cc -g  -I/usr/src/lib/libm/src -I/usr/src/lib/libm/src/ld80 -c -fpic   /usr/src/lib/libm/arch/i387/e_atan2f.S  -o e_atan2f.so"
cc -g  -I/usr/src/lib/libm/src -I/usr/src/lib/libm/src/ld80 -c -fpic   /usr/src/lib/libm/arch/i387/e_atan2f.S  -o e_atan2f.so.o
ld -X -r e_atan2f.so.o -o e_atan2f.so
rm -f e_atan2f.so.o
 :
echo building shared m library \(version 9.0\)
rm -f libm.so.9.0
cc -shared -fpic  -o libm.so.9.0  `lorder b_exp__D.so b_log__D.so ...

三回もコンパイルしてる。静的用、それのプロファイル版、そしてsoファイルとな。 ld80ってのをincludeのdirに指定してi387の下のやつをコンパイルしてるけど、これって、 出来たらインテル特製の80Bit浮動点機構を使ってねって事かしら。id128ってのも有るけど、 これは64Bitに進化した石用かな。

i387(FPU) の現場

上で作った簡単アプリは、精度が128Bit用。これじゃ、糞石のハードによる演算機構は 及びじゃない。64Bit精度にしたら、i387の出番も有るだろう、と言う目論見で、 やってみる。

出来たa.outを逆アセしてみると、

   z = cexp(x);
 b57:   8d 45 b8                lea    0xffffffb8(%ebp),%eax
 b5a:   dd 45 d0                fldl   0xffffffd0(%ebp)
 b5d:   dd 5c 24 04             fstpl  0x4(%esp)
 b61:   dd 45 d8                fldl   0xffffffd8(%ebp)
 b64:   dd 5c 24 0c             fstpl  0xc(%esp)
 b68:   89 04 24                mov    %eax,(%esp)
 b6b:   e8 40 fc ff ff          call   7b0 <__init+0x30>
 b70:   83 ec 04                sub    $0x4,%esp
 b73:   dd 45 b8                fldl   0xffffffb8(%ebp)
 b76:   dd 5d e0                fstpl  0xffffffe0(%ebp)
 b79:   dd 45 c0                fldl   0xffffffc0(%ebp)
 b7c:   dd 5d e8                fstpl  0xffffffe8(%ebp)

今までのmov一辺倒が、劇的に変わった。ニューモニックの頭に付いている、f は、 i387専用の命令だろう。

i387な石にBPを置けるか知らないけど、やってみる。

(gdb) b /usr/src/lib/libm/arch/i387/e_exp.S:52
No source file named /usr/src/lib/libm/arch/i387/e_exp.S.
Make breakpoint pending on future shared library load? (y or [n]) y

Breakpoint 1 (/usr/src/lib/libm/arch/i387/e_exp.S:52) pending.
(gdb) run
Starting program: /home/sakae/z/a.out

Breakpoint 1, exp () at /usr/src/lib/libm/arch/i387/e_exp.S:52
52              fldl    4(%esp)
(gdb) bt
#0  exp () at /usr/src/lib/libm/arch/i387/e_exp.S:52
#1  0x02b19f4b in cexp (z=0 + 3.1415926535897931 * I)
    at /usr/src/lib/libm/src/s_cexp.c:68
#2  0x14d12b70 in main () at d.c:9
 :
(gdb) si
77              fstp    %st(1)
(gdb) info float
  R7: Empty   0x3fffb8aa3b295c17f0bc
  R6: Empty   0x80000000000000000000
  R5: Empty   0x80000000000000000000
  R4: Empty   0x00000000000000000000
  R3: Empty   0x00000000000000000000
  R2: Zero    0x00000000000000000000 +0
=>R1: Valid   0x3fff8000000000000000 +1
  R0: Empty   0x3fff8000000000000000

Status Word:         0x0900                                 C0
                       TOP: 1
Control Word:        0x037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest
Tag Word:            0xffd3
Instruction Pointer: 0x00:0xfc21b6d4
Operand Pointer:     0x00:0x00000000
Opcode:              0xd9fd

へぇー、レジスターが8本と、若干の付属レジスターが有るとな。64bitの精度も 許しているのか。続けて、sinも追ってみる。

69              w = r * cos (y) + r * sin (y) * I;
(gdb) b /usr/src/lib/libm/arch/i387/s_sin.S:10
Breakpoint 2 at 0x2b31104: file /usr/src/lib/libm/arch/i387/s_sin.S, line 10.
(gdb) c
Continuing.

Breakpoint 2, sin () at /usr/src/lib/libm/arch/i387/s_sin.S:10
10              fldl    4(%esp)
(gdb) bt
#0  sin () at /usr/src/lib/libm/arch/i387/s_sin.S:10
#1  0x0ea88f69 in cexp (z=0 + 3.1415926535897931 * I)
    at /usr/src/lib/libm/src/s_cexp.c:69
#2  0x168e3b70 in main () at d.c:9
(gdb) si
11              fsin
(gdb) si
12              fnstsw  %ax
(gdb) si
13              andw    $0x400,%ax
(gdb) info float
  R7: Empty   0x3fffb8aa3b295c17f0bc
  R6: Empty   0x80000000000000000000
  R5: Empty   0x80000000000000000000
  R4: Empty   0x00000000000000000000
  R3: Empty   0x00000000000000000000
=>R2: Valid   0x3fca8d30000000000000 +1.224606353822377258e-16
  R1: Empty   0x3fff8000000000000000
  R0: Empty   0x3fff8000000000000000

Status Word:         0x1320                  PE             C0 C1
                       TOP: 2
Control Word:        0x037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest
Tag Word:            0xffcf
Instruction Pointer: 0x00:0xfc21b6d8
Operand Pointer:     0x00:0x00000000
Opcode:              0xd9fe
(gdb) c
Continuing.
real=-1 imag=1.22460635382237725821141793858259916e-16
[Inferior 1 (process 22731) exited normally]

/usr/src/lib/libm/src/s_cexp.c の sin 呼び出しが、いつの間にか、 /usr/src/lib/libm/arch/i387/s_sin.Sのマシン語呼び出しに展開されている。 その裏に、OS側が、色々やってるんだけどね。gdbで追っていくと、いやになる程、 色々なルーチンを走り回っている。この機構って、ダイナミックリンクの、ジャンプ テーブル書き換えの技が使われているのだろうね。

Linux系はどうよ?

比べてみる。

[sakae@fedora z]$ ./a.out
real=-1 imag=-5.01655761266833226942395050995492469e-20
sakae@arm:~/z$ uname -a
Linux arm 3.16.0-4-versatile #1 Debian 3.16.7-ckt9-3~deb8u1 (2015-04-24) armv5tejl GNU/Linux
sakae@arm:~/z$ ./a.out
real=-1 imag=1.2246467991473532071737640294583966e-16

armなやつは、long doubleをサポートしていないな。昔な石なんで、ソフトのサポートも サボったんかな。一応、debuggerに通してみた。

(gdb) bt
#0  __sincos (x=3.1415926535897931, sinx=0xbefff5f0, sinx@entry=0xbefff5d8,
    cosx=0xbefff5f8, cosx@entry=0xbefff5e0)
    at ../sysdeps/ieee754/dbl-64/s_sincos.c:35
#1  0xb6f67000 in __cexp (x=0 + 3.1415926535897931 * I) at s_cexp.c:44
#2  0x0001054c in main () at t.c:9

debian系はソースを取ってこないと参照出来ない。fedoraで無理して参照しようと すると、glibc-debuginfoを gdbのお供にいかがでしょうかって勧められて、それを入れると一応gdb上から 便利にソースが見られる。で、libmって、しっかり GNU Cの仲間って書いてあったぞ。

  /* Return value of complex exponential function for double complex value.
     Copyright (C) 1997-2015 Free Software Foundation, Inc.
     This file is part of the GNU C Library.
     Contributed by Ulrich Drepper <drepper@cygnus.com>, 1997.

それはいいんだけど、いきなりコードが始まっていて、数学的な根拠が何も 書いてない。なんだかなあ。

BSDはどうよ?

対抗上、今度はFreeBSDのソースを三田。どれでもいいんだけど、/usr/src/lib/msun/src/k_cos.c と言う、 cosの計算の基本となるものにした。dirツリーがlibmじゃなくて、msunになってるのは、 vimの作者であるビル・ジョイ が居た会社、Sunに敬意を表しているのかな。

 * ====================================================
 * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
 *
 * Developed at SunSoft, a Sun Microsystems, Inc. business.
 * Permission to use, copy, modify, and distribute this
 * software is freely granted, provided that this notice
 * is preserved.
 * ====================================================
     :
 * __kernel_cos( x,  y )
 * kernel cos function on [-pi/4, pi/4], pi/4 ~ 0.785398164
 * Input x is assumed to be bounded by ~pi/4 in magnitude.
 * Input y is the tail of x.
 *
 * Algorithm
 *      1. Since cos(-x) = cos(x), we need only to consider positive x.
 *      2. if x < 2^-27 (hx<0x3e400000 0), return 1 with inexact if x!=0.
 *      3. cos(x) is approximated by a polynomial of degree 14 on
 *         [0,pi/4]
 *                                       4            14
 *              cos(x) ~ 1 - x*x/2 + C1*x + ... + C6*x
 *         where the remez error is
 *
 *      |              2     4     6     8     10    12     14 |     -58
 *      |cos(x)-(1-.5*x +C1*x +C2*x +C3*x +C4*x +C5*x  +C6*x  )| <= 2
 *      |                                                      |
 *
 *                     4     6     8     10    12     14
 *      4. let r = C1*x +C2*x +C3*x +C4*x +C5*x  +C6*x  , then
 *             cos(x) ~ 1 - x*x/2 + r
 *         since cos(x+y) ~ cos(x) - sin(x)*y
 *                        ~ cos(x) - x*y,
 *         a correction term is necessary in cos(x) and hence
 *              cos(x+y) = 1 - (x*x/2 - (r - x*y))
 *         For better accuracy, rearrange to
 *              cos(x+y) ~ w + (tmp + (r-x*y))
 *         where w = 1 - x*x/2 and tmp is a tiny correction term
 *         (1 - x*x/2 == w + tmp exactly in infinite precision).
     :

whereとかletなんて単語が出てきて、何となくHaskellっぽい雰囲気で説明が 続いている。ちゃんと根拠が有るのは、産業界では必須な事ですよ。

ここまでは良いんだけど、NetBSDで試してみたら、cexplは受け付けてくれるものの 精度はdoubleでの計算しか出来ずのズルモードが発動しました。 FreeBSDでは、コンパイルの段階で、complex.hをincludeせいや、あるいはテメーで 定義しろやと抜かしおって、バイナリーを作ってくれず。同じ血を分けたBSD3兄弟と 言えども、性格が出てて面白い。

Intel 8087

FreeBSD上の呼称は i387 一般的にはFPU。もっと一般化すると、コ・プロセッサーに なるかな。armとかmipsな石だと、コプロとか言うね。

Intel 8087(日本語版の概略説明)

Intel 8087 (英語版、詳しい説明有り)

x87 FPUプログラミング

IA32 x87命令一覧 x87 FPU