Как временные среды Java, нацеленные на процессоры pre-SSE2, реализуют базовые операции с плавающей запятой?

Как работает Java-среда выполнения, ориентированная на процессор Intel без SSE2, с денормалами с плавающей запятой, когда установлен strictfp?

Даже когда 387 FPU установлен для 53-битной точности, он сохраняет диапазон негабаритных экспонентов, который:

  • принудительно обнаруживает переполнение/переполнение при каждом промежуточном результате и
  • затрудняет избежание двойного округления денормалов.

Стратегии включают повторное вычисление операции, которая привела к денормальному значению с эмулируемой плавающей запятой или постоянному смещению экспоненты по линиям этой техники для оснащения OCaml с 63-битными поплавками, заимствуя бит от экспоненты, чтобы избежать двойного округления.

В любом случае я не вижу возможности избежать хотя бы одной условной ветки для каждого вычисления с плавающей запятой, если только эта операция не может быть определена статически не для переполнения/переполнения. Как рассматриваются исключительные (переполнение/недоиспользование) случаи, это часть моего вопроса, но это не может быть отделено от вопроса о представлении (постоянная стратегия смещения экспонентов, по-видимому, означает, что только переполнение должно быть например, для проверки).

Ответ 1

Мне кажется, из очень тривиального тестового примера, как JVM round-trip каждые double вычисление через память, чтобы получить округление, которое он хочет. Похоже, что он делает что-то странное с парой волшебных констант. Вот что он сделал для меня для простой "вычислительной 2 ^ n наивной" программы:

0xb1e444b0: fld1
0xb1e444b2: jmp    0xb1e444dd         ;*iload
                                      ; - fptest::[email protected] (line 6)
0xb1e444b7: nop
0xb1e444b8: fldt   0xb523a2c8         ;   {external_word}
0xb1e444be: fmulp  %st,%st(1)
0xb1e444c0: fmull  0xb1e44490         ;   {section_word}
0xb1e444c6: fldt   0xb523a2bc         ;   {external_word}
0xb1e444cc: fmulp  %st,%st(1)
0xb1e444ce: fstpl  0x10(%esp)
0xb1e444d2: inc    %esi               ; OopMap{off=51}
                                      ;*goto
                                      ; - fptest::[email protected] (line 6)
0xb1e444d3: test   %eax,0xb3f8d100    ;   {poll}
0xb1e444d9: fldl   0x10(%esp)         ;*goto
                                      ; - fptest::[email protected] (line 6)
0xb1e444dd: cmp    %ecx,%esi
0xb1e444df: jl     0xb1e444b8         ;*if_icmpge
                                      ; - fptest::[email protected] (line 6)

Я считаю, что 0xb523a2c8 и 0xb523a2bc являются _fpu_subnormal_bias1 и _fpu_subnormal_bias2 из исходного кода точки доступа. _fpu_subnormal_bias1 выглядит как 0x03ff8000000000000000, а _fpu_subnormal_bias2 выглядит 0x7bff8000000000000000. _fpu_subnormal_bias1 имеет эффект масштабирования наименьшей нормальной double до наименьшей нормальной long double; если FPU округляется до 53 бит, произойдет "правильная вещь".

Я бы предположил, что существует, казалось бы, бессмысленная инструкция test, чтобы поток можно было прервать, пометив эту страницу нечитаемой в случае необходимости GC.

Здесь код Java:

import java.io.*;
public strictfp class fptest {
 public static double calc(int k) {
  double a = 2.0;
  double b = 1.0;
  for (int i = 0; i < k; i++) {
   b *= a;
  }
  return b;
 }
 public static double intest() {
  double d = 0;
  for (int i = 0; i < 4100; i++) d += calc(i);
  return d;
 }
 public static void main(String[] args) throws Exception {
  for (int i = 0; i < 100; i++)
   System.out.println(intest());
 }
}

Копаем дальше, код для этих операций находится на виду в коде OpenJDK в hotspot/src/cpu/x86/vm/x86_63.ad. Соответствующие фрагменты:

instruct strictfp_mulD_reg(regDPR1 dst, regnotDPR1 src) %{
  predicate( UseSSE<=1 && Compile::current()->has_method() && Compile::current()
->method()->is_strict() );
  match(Set dst (MulD dst src));
  ins_cost(1);   // Select this instruction for all strict FP double multiplies

  format %{ "FLD    StubRoutines::_fpu_subnormal_bias1\n\t"
            "DMULp  $dst,ST\n\t"
            "FLD    $src\n\t"
            "DMULp  $dst,ST\n\t"
            "FLD    StubRoutines::_fpu_subnormal_bias2\n\t"
            "DMULp  $dst,ST\n\t" %}
  opcode(0xDE, 0x1); /* DE C8+i or DE /1*/
  ins_encode( strictfp_bias1(dst),
              Push_Reg_D(src),
              OpcP, RegOpc(dst),
              strictfp_bias2(dst) );
  ins_pipe( fpu_reg_reg );
%}

instruct strictfp_divD_reg(regDPR1 dst, regnotDPR1 src) %{
  predicate (UseSSE<=1);
  match(Set dst (DivD dst src));
  predicate( UseSSE<=1 && Compile::current()->has_method() && Compile::current()
->method()->is_strict() );
  ins_cost(01);

  format %{ "FLD    StubRoutines::_fpu_subnormal_bias1\n\t"
            "DMULp  $dst,ST\n\t"
            "FLD    $src\n\t"
            "FDIVp  $dst,ST\n\t"
            "FLD    StubRoutines::_fpu_subnormal_bias2\n\t"
            "DMULp  $dst,ST\n\t" %}
  opcode(0xDE, 0x7); /* DE F8+i or DE /7*/
  ins_encode( strictfp_bias1(dst),
              Push_Reg_D(src),
              OpcP, RegOpc(dst),
              strictfp_bias2(dst) );
  ins_pipe( fpu_reg_reg );
%}

Я ничего не вижу для сложения и вычитания, но я бы сказал, что они просто делают добавление/вычитание с FPU в 53-битном режиме, а затем округляют результат через память. Мне немного любопытно, есть ли сложный случай переполнения, что они ошибаются, но мне не любопытно узнать.