Skip to content

Point R : sortie anticipée de hausdorff() à borne infinie, atanh_rel testé à borne infinie - #92

Open
Jordan08 wants to merge 2 commits into
configure-cleanfrom
todo-r-atanh-hausdorff
Open

Jordan08 wants to merge 2 commits into
configure-cleanfrom
todo-r-atanh-hausdorff

Conversation

@Jordan08

@Jordan08 Jordan08 commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Point R de TODO.md : la sortie anticipée de hausdorff() quand une borne n'est infinie que d'un côté, et un TEST_EQ d'atanh_rel à borne infinie (anciens 50 et 7).

R.50 : hausdorff() à borne infinie

Problème. Depuis #41 (point 10), hausdorff() calcule toutes ses distances par une seule formule, après la vérification du sens d'arrondi (GAOL_RND_ENTER()) : max(d(a, c), d(b, e)), avec d(x, y) = 0 si x == y. Le cas d'une borne infinie dans un seul des deux intervalles, qui vaut +oo, paie donc la vérification et les différences : de 10 à 12 ns, contre 2 à 4 ns s'il sort d'emblée (mesures ci-dessous). GAOL 4 rendait +oo d'emblée pour toute borne infinie.

Correction (décidée le 3 octobre). Dans gaol/gaol_interval_sse.cpp et gaol/gaol_interval_fpu.cpp, après le test du vide et avant GAOL_RND_ENTER() :

if ((std::fabs(I1.left()) == GAOL_INFINITY) != (std::fabs(I2.left()) == GAOL_INFINITY)
    || (std::fabs(I1.right()) == GAOL_INFINITY) != (std::fabs(I2.right()) == GAOL_INFINITY)) {
  return GAOL_INFINITY;
}
  • Le même résultat qu'avant. Quand une seule des deux bornes d'un côté est infinie, la formule donnait déjà +oo par inf - x, et le maximum avec l'autre côté reste +oo. Deux bornes infinies égales passent toujours par la formule et sont à distance 0 : hausdorff([1, +oo], [2, +oo]) vaut 1. La phrase du commentaire de la formule sur ce cas, qu'elle ne voit plus, est retirée.
  • Indépendant du sens d'arrondi et des modes flush-to-zero et denormals-are-zero. Savoir si une borne est infinie ne dépend ni de l'un ni des autres : ces modes lisent un sous-normal comme un zéro, qui n'est pas infini non plus.
  • Les bornes que compare la formule sont lues après la vérification : avec GCC et Clang, GAOL_RND_BARRIER() force leur relecture ; avec Visual C++, c'est /fp:strict qui garde les comparaisons après l'écriture du registre de contrôle.
  • Documentation. doc/using.md et le manuel ajoutent ce test à celui du vide, parmi les comparaisons faites avant la vérification.
  • Aucune exception flottante. Il n'y a que des comparaisons avec l'infini, sans arithmétique, et le test du vide a écarté les bornes NaN. Le relecteur l'a vérifié : FE_INVALID n'est levée dans aucun des 7 cas à borne infinie essayés ; la sortie anticipée ne lève pas non plus l'inexact, que la base levait.
  • Sens d'arrondi. Sans GAOL_PRESERVE_ROUNDING, la sortie anticipée laisse le sens d'arrondi tel qu'elle l'a trouvé, ce que tests/rounding_direction.cpp admet (« unchanged or upward »).
  • std::fabs(x) == GAOL_INFINITY plutôt que std::isinf(x), sur la remarque du relecteur : c'est une comparaison sur tout compilateur. Avec l'UCRT de Visual C++, std::isinf() pourrait être un appel de fonction (_dtest()), comme l'étaient les comparaisons silencieuses de <cmath> (Point D (24 et 62) : plus d'exception flottante parasite, et leur documentation #83) : je ne l'ai pas vérifié ici.
  • Écarté. Une écriture sans court-circuit (| au lieu de ||) a été mesurée : elle n'est pas plus rapide.

Mesures. Le banc fait la médiane de 5 mesures sur 1024 paires, en ns par appel (Intel i7-1185G7, Release, banc en -O2, liens avec le GAOL installé). Le tableau donne le minimum de 10 passes alternant base et branche, pour l'écriture finale. Deux séries faites avec la première écriture (std::isinf()) avaient donné les mêmes chiffres l'une que l'autre à 0,1 ns près, et des chiffres à 1 ns au plus de ceux-ci (sur des bornes finies en SSE2 : 10,38 ns avec std::isinf(), 11,36 avec std::fabs()).

bornes finies [a, +oo], [c, d] [-oo, b], [c, d] [a, +oo], [c, +oo]
Clang 18 (SSE2), base → branche 10,37 → 11,03 10,07 → 3,68 10,40 → 2,35 7,80 → 8,43
GCC 9.4, FPU 9,37 → 10,52 11,36 → 4,48 11,13 → 3,72 7,69 → 9,08
GCC 9.4, SSE2 11,27 → 11,36 11,85 → 3,94 11,38 → 3,38 7,79 → 8,44
  • Le cas visé est de 2,5 à 4,4 fois plus rapide.
  • Les autres cas paient le test : de 0,1 à 1,2 ns sur des bornes finies, et jusqu'à 1,4 ns quand les deux intervalles ont une borne infinie du même côté.
  • Le relecteur, avec son propre banc, a mesuré environ 8 ns sur la base et 3 ns avec la branche, et aucun écart hors du bruit sur des bornes finies. Le commentaire du code dit donc « deux à quatre fois plus long », sans valeur absolue.

Tests. Le comportement ne change pas : la sortie anticipée est couverte par hausdorff_of_infinite_bounds() (tests/other_functions.cpp, 1225 paires de bornes dans {-oo, -2, -1, -0, 0, 1, 2, +oo}, contre des distances exactes) et par les vérifications de GAOL 4 dans tests/interval_functions.cpp. Pour montrer qu'ils la protègent, deux mutants de la sortie anticipée, en SSE2 et en FPU :

  • renvoyer 0 au lieu de +oo : 670 échecs dans other_functions, 12 dans interval_functions ;
  • la règle de GAOL 4, +oo dès qu'une borne est infinie : 80 échecs dans other_functions, 3 dans interval_functions ;
  • correction rétablie : aucun échec (652 093 et 126 vérifications).

Ne tester qu'un des deux côtés est un mutant équivalent : le résultat est le même, seul le temps change.

R.7 : atanh_rel à borne infinie

tests/reverse_mappings.cpp vérifiait atanh_rel([0.5, 1], [0, 100]), où l'intersection avec un I borné cache la borne +oo de la préimage. Depuis #41, TEST_EQ (qui compare par hausdorff() <= 1e-8) peut comparer deux bornes infinies égales. Les vérifications ajoutées :

  • TEST_EQ(atanh_rel([0.5, 1], [0, +oo]), [atanh(0.5), +oo]) ;
  • son miroir, TEST_EQ(atanh_rel([-1, -0.5], [-oo, 0]), [-oo, -atanh(0.5)]).

La valeur de référence, 0.5493061443340548457, vient de mpmath à 2000 bits. doc/tests.md le dit dans l'entrée des tests de GAOL 4.

Dents. Avec un atanh() modifié pour borner la préimage par ±MAX au lieu de ±oo aux extrémités ±1, en SSE2 et en FPU :

  • les deux nouvelles vérifications échouent ([0x1.193ea7aad030ap-1, 0x1.fffffffffffffp+1023] contre [0x1.193ea7aad030bp-1, inf]) ;
  • le reverse_mappings.cpp de configure-clean passe (584 vérifications, aucun échec) ;
  • elementary voit lui aussi 6 échecs, sur atanh() elle-même.

L'autre moitié de R.7, atanh([1]) et atanh([-1]) dans doc/compare/special_cases.md, est laissée au point 33, comme décidé le 3 octobre.

Pour doc/differences.md et ChangeLog (pull request de synthèse)

doc/differences.md, dans la puce de hausdorff(), celle de #41 qui sera fusionnée avec la puce de la l. 288 :

A bound infinite in one interval only gives +oo before the check of the rounding direction, two to four times as fast as through the computation of the distance (point R.50).

ChangeLog :

* gaol/gaol_interval_sse.cpp, gaol/gaol_interval_fpu.cpp (hausdorff): +oo returned before the check of the rounding direction when a bound is infinite in one interval only.
* tests/reverse_mappings.cpp: atanh_rel compared at its infinite ends.

Relecture

Un relecteur indépendant, sans le contexte de ce travail, a relu la branche avec process.md. Verdict : approuvée avec remarques, aucun point bloquant.

Ce qu'il a vérifié (hors de l'arbre) :

  • Résultats identiques bit à bit. hausdorff() sur toutes les paires de 66 intervalles (±0, ±3·2^-1074, ±1, 2, ±MAX, ±oo), dans les 4 sens d'arrondi et les 4 états de MXCSR (aucun mode, FTZ, DAZ, les deux), soit 69 696 résultats : identiques entre base et branche, avec GCC et Clang, en FPU et avec GAOL_PRESERVE_ROUNDING.
  • Exceptions flottantes : aucune FE_INVALID sur des bornes infinies.
  • Code machine : GCC relit les bornes après la vérification (désassemblage à -O3).
  • Tests : other_functions, interval_functions, reverse_mappings et rounding_direction passent en SSE2, en FPU et avec GAOL_PRESERVE_ROUNDING, et compilent avec -Werror.
  • Mutants : ceux de la sortie anticipée et d'atanh() sont attrapés.
  • Manuel : il compile.

Ses remarques, toutes corrigées :

  • le chiffre du commentaire (10 à 12 ns), qu'il ne retrouvait pas avec son banc : le commentaire donne maintenant le rapport ;
  • la phrase du commentaire de la formule devenue sans objet, retirée ;
  • la relecture des bornes après la vérification, rapportée à GAOL_RND_BARRIER() et à /fp:strict ;
  • std::isinf(), peut-être un appel avec Visual C++, remplacé par std::fabs(x) == GAOL_INFINITY ;
  • des formulations : « as GAOL 4 did » alors que GAOL 4 sortait pour toute borne infinie, un « (GAOL v5) » retiré de doc/using.md pour s'accorder au manuel, « a bounded I », « atanh_rel(J, I) ».

Sa dernière remarque est reportée aux questions ouvertes.

Vérifié en local

Sur le code final :

  • CMake avec tests et exemples, GCC 9.4 SSE2, GCC 9.4 FPU (-DGAOL_SIMD=OFF) et Clang 18 : 56 tests réussis sur 56 dans chacun (intervalf et interval2f sautés par construction).
  • -Wall -Wextra avec CMAKE_COMPILE_WARNING_AS_ERROR=ON, GCC 9.4 et Clang 18 : aucun avertissement, 56 sur 56.
  • GAOL_PRESERVE_ROUNDING=ON, GCC SSE2 et Clang 18 FPU : 40 tests réussis sur 40 dans chacun.
  • autotools : 28 tests réussis et 2 sautés, plus 16 exemples ; make distclean rend l'arbre tel que git l'a.
  • meson 0.53.2 : 44 réussis et 2 sautés.
  • Manuel : deux passes de pdflatex, 129 pages, aucune erreur. Pas de nouvel « Overfull » : le seul était déjà dans la base, c'est celui que le point Y doit corriger, aux l. 525-529 de gaol.tex aujourd'hui.
  • check_branch : OK.

Non vérifiable ici : Windows (Visual C++, où GAOL_RND_BARRIER() est vide et où le coût de std::fabs() n'est pas mesuré), MinGW-w64, macOS, ARM, POWER, s390x, et les processeurs sans FMA.

Dépendances

Aucune.

Questions ouvertes

  • doc/using.md (l. 445, « Each operation of GAOL sets the rounding direction upward when it is not, and leaves it upward ») et le manuel ne disent pas que les opérations qui rendent avant leur vérification laissent le sens d'arrondi tel qu'elles l'ont trouvé : celles qui ont un opérande vide, avant ce changement, et maintenant hausdorff() à borne infinie d'un seul côté. Seul le paragraphe sur les modes flush-to-zero en parle. Je n'y ai pas touché.
  • La clause J.right() == -1.0 d'atanh() (gaol/gaol_interval.cpp) ne fait échouer aucun test si on la retire : atanh([-1]), atanh([-5, -1]) et atanh([-oo, -1]) sont vides de toute façon, interval(-oo, -oo) l'étant. Un test ne la protégerait que si cette règle du constructeur changeait. Je n'y ai pas touché.
  • atanh_rel() vérifie le sens d'arrondi puis appelle atanh(), qui le vérifie à nouveau, comme les cas que relève le point B.2. Je n'y ai pas touché.

@Jordan08

Jordan08 commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

Décidé le 6 octobre, sur les questions ouvertes, à reporter dans TODO.md après la fusion :

  • La phrase de doc/using.md (l. 445) et du manuel selon laquelle chaque opération laisse le sens d'arrondi vers le haut sera complétée au point I : les opérations qui rendent avant leur vérification (un opérande vide, et hausdorff() quand une borne n'est infinie que d'un côté) le laissent tel qu'elles l'ont trouvé.
  • La double vérification du sens d'arrondi de asinh_rel() et atanh_rel(), qui vérifient le sens puis appellent asinh() et atanh(), qui le vérifient à nouveau, va au point B.2. acos_rel(), asin_rel() et atan_rel() n'en font qu'une.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant