(спойлер) дебажил, дизасемблил и пришел к выводу что проблема в SSE инструкциях

Привет, Хабр!

Все началось с того что я писал Load тест на Java для внутреннего компонента системы над которой сейчас работаю. Тест создавал несколько потоков и пытался что-то выполнить очень много раз. В процессе выполнения иногда появлялись java.lang.ArrayIndexOutOfBoundsException: 0 ошибки на строчке очень похожей на эту:

"test".getBytes(StandardCharsets.UTF_8)

Строчка конечно была другая, но после небольшого изучения удалось найти проблему именно в ней. В итоге был написан JMH бенчмарк:

@Benchmark
public byte[] originalTest() {
  return "test".getBytes(StandardCharsets.UTF_8);
}

Который падал после нескольких секунд работы со следующим исключением:

java.lang.ArrayIndexOutOfBoundsException: 0
	at sun.nio.cs.UTF_8$Encoder.encode(UTF_8.java:716)
	at java.lang.StringCoding.encode(StringCoding.java:364)
	at java.lang.String.getBytes(String.java:941)
	at org.sample.MyBenchmark.originalTest(MyBenchmark.java:41)
	at org.sample.generated.MyBenchmark_originalTest.originalTest_thrpt_jmhLoop(MyBenchmark_originalTest.java:103)
	at org.sample.generated.MyBenchmark_originalTest.originalTest_Throughput(MyBenchmark_originalTest.java:72)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.openjdk.jmh.runner.LoopBenchmarkHandler$BenchmarkTask.call(LoopBenchmarkHandler.java:210)
	at org.openjdk.jmh.runner.LoopBenchmarkHandler$BenchmarkTask.call(LoopBenchmarkHandler.java:192)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

Я никогда не сталкивался с таким раньше, поэтому попробовал тривиальные решения вроде обновить JVM и перезагрузить компьютер, но это, разумеется, не помогло. Проблема возникала на моем MacBook Pro (13-inch, 2017) 3,5 GHz Intel Core i7 и не повторялась на машинах коллег. Не найдя других факторов я решил изучать код дальше.

Проблема возникала внутри JVM класса StringCoding в методе encode():

private static int scale(int len, float expansionFactor) {
    // We need to perform double, not float, arithmetic; otherwise
    // we lose low order bits when len is larger than 2**24.
    return (int)(len * (double)expansionFactor);
}

static byte[] encode(Charset cs, char[] ca, int off, int len) {
    CharsetEncoder ce = cs.newEncoder();
    int en = scale(len, ce.maxBytesPerChar());
    byte[] ba = new byte[en];
    if (len == 0)
        return ba;
...
}

Массив ba в редких случаях создавался длинной в 0 элементов и это и вызывало ошибку в дальнейшем.

Я попробовал убрав зависимость от UTF_8, но это не получилось. Зависимость пришлось оставить, иначе проблема не воспроизводилась, но получилось убрать много лишнего:

private static int encode() {
    return (int) ((double) StandardCharsets.UTF_8.newEncoder().maxBytesPerChar());
}

maxBytesPerChar возвращает константу из final поля равную 3.0, но сам метод в редких случаях (1 на 1000000000) возвращал 0. Вдвойне странно было то, что убрав каст в double метод отрабатывал как надо во всех случаях.

Я добавил опции JIT компилятора -XX:-TieredCompilation и -client но это никак не повлияло. В итоге я собрал hsdis-amd64.dylib под Мак, добавил опции -XX:PrintAssemblyOptions=intel, -XX:CompileCommand=print,*MyBenchmark.encode и -XX:CompileCommand=dontinline,*MyBenchmark.encode и начал сравнивать сгенерированный JIT'ом ассемблер для метода с кастом в double и без:

НЕ рабочий вариант с кастом:
0x000000010a44e3ca: mov    rbp,rax            ;*synchronization entry
                                            ; - sun.nio.cs.UTF_8$Encoder::<init>@-1 (line 558)
                                            ; - sun.nio.cs.UTF_8$Encoder::<init>@2 (line 554)
                                            ; - sun.nio.cs.UTF_8::newEncoder@6 (line 72)
                                            ; - org.sample.MyBenchmark::encode@3 (line 50)

0x000000010a44e3cd: movabs rdx,0x76ab16350    ;   {oop(a 'sun/nio/cs/UTF_8')}
0x000000010a44e3d7: vmovss xmm0,DWORD PTR [rip+0xffffffffffffff61]        # 0x000000010a44e340 
                                            ;   {section_word}
0x000000010a44e3df: vmovss xmm1,DWORD PTR [rip+0xffffffffffffff5d]        # 0x000000010a44e344
                                            ;   {section_word}
0x000000010a44e3e7: mov    rsi,rbp
0x000000010a44e3ea: nop
0x000000010a44e3eb: call   0x000000010a3f40a0  ; OopMap{rbp=Oop off=144}
                                            ;*invokespecial <init>
                                            ; - sun.nio.cs.UTF_8$Encoder::<init>@6 (line 558)
                                            ; - sun.nio.cs.UTF_8$Encoder::<init>@2 (line 554)
                                            ; - sun.nio.cs.UTF_8::newEncoder@6 (line 72)
                                            ; - org.sample.MyBenchmark::encode@3 (line 50)
                                            ;   {optimized virtual_call}
0x000000010a44e3f0: mov    BYTE PTR [rbp+0x2c],0x3f  ;*new
                                            ; - sun.nio.cs.UTF_8::newEncoder@0 (line 72)
                                            ; - org.sample.MyBenchmark::encode@3 (line 50)

0x000000010a44e3f4: vcvtss2sd xmm0,xmm0,DWORD PTR [rbp+0x10]
0x000000010a44e3f9: vcvttsd2si eax,xmm0
0x000000010a44e3fd: cmp    eax,0x80000000
0x000000010a44e403: jne    0x000000010a44e414
0x000000010a44e405: sub    rsp,0x8
0x000000010a44e409: vmovsd QWORD PTR [rsp],xmm0
0x000000010a44e40e: call   Stub::d2i_fixup    ;   {runtime_call}
0x000000010a44e413: pop    rax                ;*d2i  ; - org.sample.MyBenchmark::encode@10 (line 50)

0x000000010a44e414: add    rsp,0x20
0x000000010a44e418: pop    rbp

Рабочий вариант без каста:
0x000000010ef7e04a: mov    rbp,rax            ;*synchronization entry
                                                ; - sun.nio.cs.UTF_8$Encoder::<init>@-1 (line 558)
                                                ; - sun.nio.cs.UTF_8$Encoder::<init>@2 (line 554)
                                                ; - sun.nio.cs.UTF_8::newEncoder@6 (line 72)
                                                ; - org.sample.MyBenchmark::encode@3 (line 50)

0x000000010ef7e04d: movabs rdx,0x76ab16350    ;   {oop(a 'sun/nio/cs/UTF_8')}
0x000000010ef7e057: vmovss xmm0,DWORD PTR [rip+0xffffffffffffff61]        # 0x000000010ef7dfc0
                                            ;   {section_word}
0x000000010ef7e05f: vmovss xmm1,DWORD PTR [rip+0xffffffffffffff5d]        # 0x000000010ef7dfc4
                                            ;   {section_word}
0x000000010ef7e067: mov    rsi,rbp
0x000000010ef7e06a: nop
0x000000010ef7e06b: call   0x000000010ef270a0  ; OopMap{rbp=Oop off=144}
                                            ;*invokespecial <init>
                                            ; - sun.nio.cs.UTF_8$Encoder::<init>@6 (line 558)
                                            ; - sun.nio.cs.UTF_8$Encoder::<init>@2 (line 554)
                                            ; - sun.nio.cs.UTF_8::newEncoder@6 (line 72)
                                            ; - org.sample.MyBenchmark::encode@3 (line 50)
                                            ;   {optimized virtual_call}
0x000000010ef7e070: mov    BYTE PTR [rbp+0x2c],0x3f  ;*new
                                            ; - sun.nio.cs.UTF_8::newEncoder@0 (line 72)
                                            ; - org.sample.MyBenchmark::encode@3 (line 50)

0x000000010ef7e074: vmovss xmm1,DWORD PTR [rbp+0x10]
0x000000010ef7e079: vcvttss2si eax,xmm1
0x000000010ef7e07d: cmp    eax,0x80000000
0x000000010ef7e083: jne    0x000000010ef7e094
0x000000010ef7e085: sub    rsp,0x8
0x000000010ef7e089: vmovss DWORD PTR [rsp],xmm1
0x000000010ef7e08e: call   Stub::f2i_fixup    ;   {runtime_call}
0x000000010ef7e093: pop    rax                ;*f2i  ; - org.sample.MyBenchmark::encode@9 (line 50)

0x000000010ef7e094: add    rsp,0x20
0x000000010ef7e098: pop    rbp

Одно из отличий было наличие инструкций vcvtss2sd и vcvttsd2si. Я переключился на C++ и решил воспроизвести последовательность на inline asm, но в процессе отладки выяснилось что clang компилятор с опцией -O0 использует cvtss2sd инструкцию при сравнении float != 1.0. В итоге все свелось к функции compare:

/*
 * sse
    0x105ea2f30 <+0>:  pushq  %rbp
    0x105ea2f31 <+1>:  movq   %rsp, %rbp

    0x105ea2f34 <+4>:  movsd  0x6c(%rip), %xmm0         ; xmm0 = mem[0],zero
    0x105ea2f3c <+12>: movss  0x6c(%rip), %xmm1         ; xmm1 = mem[0],zero,zero,zero
    0x105ea2f44 <+20>: movss  %xmm1, -0x4(%rbp)
->  0x105ea2f49 <+25>: cvtss2sd -0x4(%rbp), %xmm1
    0x105ea2f4e <+30>: ucomisd %xmm0, %xmm1

    0x105ea2f52 <+34>: setne  %al
    0x105ea2f55 <+37>: setp   %cl
    0x105ea2f58 <+40>: orb    %cl, %al
    0x105ea2f5a <+42>: andb   $0x1, %al
    0x105ea2f5c <+44>: movzbl %al, %eax
    0x105ea2f5f <+47>: popq   %rbp
    0x105ea2f60 <+48>: retq
    0x105ea2f61 <+49>: nopw   %cs:(%rax,%rax)
 */

bool compare() {
    float val = 1.0;
    return val != 1.0;
}

И эта функция в редких случаях возвращала false. Я написал небольшую обертку чтобы сосчитать процент ошибочных выполнений:

int main() {
    int error = 0;
    int secondCompareError = 0;

    for (int i = 0; i < INT_MAX; i++) {
        float result = 1.0;

        if (result != 1.0) {
            error++;

            if (result != 1.0) {
                secondCompareError++;
            }
        }
    }

    std::cout << "Iterations: " << INT_MAX
              << ", errors: " << error
              <<", second compare errors: " << secondCompareError
              << std::endl;
    return 0;
}

Результат был следующий: Iterations: 2147483647, errors: 111, second compare errors: 0. Интересно то, что повторная проверка никогда не выдавала ошибки.

Я отключил поддержку SSE у clang, функция compare стала выглядеть так:

/*
 * no sse
    0x102745f50 <+0>:  pushq  %rbp
    0x102745f51 <+1>:  movq   %rsp, %rbp

    0x102745f54 <+4>:  movl   $0x3f800000, -0x4(%rbp)   ; imm = 0x3F800000
->  0x102745f5b <+11>: flds   -0x4(%rbp)
    0x102745f5e <+14>: fld1
    0x102745f60 <+16>: fxch   %st(1)
    0x102745f62 <+18>: fucompi %st(1)
    0x102745f64 <+20>: fstp   %st(0)

    0x102745f66 <+22>: setp   %al
    0x102745f69 <+25>: setne  %cl
    0x102745f6c <+28>: orb    %al, %cl
    0x102745f6e <+30>: andb   $0x1, %cl
    0x102745f71 <+33>: movzbl %cl, %eax
    0x102745f74 <+36>: popq   %rbp
    0x102745f75 <+37>: retq
    0x102745f76 <+38>: nopw   %cs:(%rax,%rax)
 */
bool compare() {
    float val = 1.0;
    return val != 1.0;
}

И проблема больше не воспроизводилась. Из этого я могу сделать выводы что набор SSE инструкция не очень хорошо работает на моей системе.

Я работаю программистом больше 7 лет, а программирую больше 16 и за это время я привык доверять примитивным операциям. Она всегда работает и результат всегда одинаковый. Осознать что сравнение float'а в какой то момент может сломаться это конечно шок. И что с этим можно сделать кроме как заменить Мак не ясно.

Комментарии (52)


  1. Sabubu
    19.06.2018 18:43

    Производителю процессора сообщили?


    1. SergeyMax
      19.06.2018 18:58
      +2

      Я бы начал с прогона мемтеста)


      1. fls_welvet Автор
        19.06.2018 19:19
        +1

        Спасибо за совет, попробую поставить built-in тест на ночь.


    1. Garbus
      19.06.2018 19:09

      И еще интересно, это проблема конкретного экземпляра процессора, партии и т.д.? Все же нынче некоторый процент брака стал нормой, вдруг просто проскочил при тестировании.


  1. kemsky
    19.06.2018 18:46

    Интересная проблема, попробуйте с 'strictfp' в Java или с аналогичными опциями clang. Смущает только, что слишком большая разница, как для ошибки округления, я бы продолжил поиски)


    1. fls_welvet Автор
      19.06.2018 19:03

      Честно говоря не знаю куда дальше продолжать :)

      Добавление strictfp к бенчмарк классу в Java ошибку не убрало. Не уверен как это можно включить для clang. В интернете я нашел что можно проверить std::numeric_limits::is_iec559 — «возвращает» 1.


  1. vladimir_dolzhenko
    19.06.2018 19:19

    Интересно какая версия java? Кто вендор? Спрашиваю из соображений проверить на своём макбуке


    1. fls_welvet Автор
      19.06.2018 19:23

      Версия HotSpot java version «1.8.0_171».
      У коллег проблема не повторялась. Исходя из того что проблема появлялась очень редко и воспроизвелась на простом cpp методе, я думаю что дело именно в железе конкретного компьютера.


  1. kibb
    19.06.2018 19:47
    +2

    Я бы начал с того, что обновил операционку. Такая проблема скорее похожа на ошибку сохранения/восстановления контекста FPU, а не аппаратную. Кроме того, поскольку у вас есть изолированный пример на С++, попробуйте загрузить live-CD с линуксом и проверьте на этом же железе, но с другой OS.


    1. fls_welvet Автор
      19.06.2018 20:39

      Спасибо за совет.

      — Обновить ОС не помогло.
      — Я попробовал запустить докер контейнер с убунту и собрать бинарник там. Внутри контейнера ошибка не повторилась! (проверил gdb что sse инструкции используются). Проверить на реальном live cd пока нет возможности так как на ноутбуке только usb-c порты :( Проверю на «чистом» линуксе как появится возможность.


      1. vassabi
        20.06.2018 02:18

        Внутри контейнера ошибка не повторилась! (проверил gdb что sse инструкции используются).
        если не повторилась — значит точно не железо, а операционка.
        Странно только почему (т.е. какая версия+билд ОС и какая версия фирмвари в ЦПУ)…


        1. fls_welvet Автор
          20.06.2018 09:10

          Версии
          ?  ~ system_profiler SPSoftwareDataType
          Software:
          
              System Software Overview:
          
                System Version: macOS 10.13.5 (17F77)
                Kernel Version: Darwin 17.6.0
                Boot Volume: Macintosh HD
                Boot Mode: Normal
                Computer Name: MacBook Pro (46)
                User Name: Aleksei Kutuzov (alekseik)
                Secure Virtual Memory: Enabled
                System Integrity Protection: Enabled
                Time since boot: 12:39
          
          ?  ~ sysctl -a | grep machdep.cpu
          
          
          machdep.cpu.max_basic: 22
          machdep.cpu.max_ext: 2147483656
          machdep.cpu.vendor: GenuineIntel
          machdep.cpu.brand_string: Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz
          machdep.cpu.family: 6
          machdep.cpu.model: 142
          machdep.cpu.extmodel: 8
          machdep.cpu.extfamily: 0
          machdep.cpu.stepping: 9
          machdep.cpu.feature_bits: 9221959987971750911
          machdep.cpu.leaf7_feature_bits: 43804591
          machdep.cpu.extfeature_bits: 1241984796928
          machdep.cpu.signature: 526057
          machdep.cpu.brand: 0
          machdep.cpu.features: FPU VME DE PSE TSC MSR PAE MCE CX8 APIC SEP MTRR PGE MCA CMOV PAT PSE36 CLFSH DS ACPI MMX FXSR SSE SSE2 SS HTT TM PBE SSE3 PCLMULQDQ DTES64 MON DSCPL VMX EST TM2 SSSE3 FMA CX16 TPR PDCM SSE4.1 SSE4.2 x2APIC MOVBE POPCNT AES PCID XSAVE OSXSAVE SEGLIM64 TSCTMR AVX1.0 RDRAND F16C
          machdep.cpu.leaf7_features: SMEP ERMS RDWRFSGS TSC_THREAD_OFFSET BMI1 AVX2 BMI2 INVPCID SMAP RDSEED ADX IPT SGX FPU_CSDS MPX CLFSOPT
          machdep.cpu.extfeatures: SYSCALL XD 1GBPAGE EM64T LAHF LZCNT PREFETCHW RDTSCP TSCI
          machdep.cpu.logical_per_package: 16
          machdep.cpu.cores_per_package: 8
          machdep.cpu.microcode_version: 132
          machdep.cpu.processor_flag: 6
          machdep.cpu.mwait.linesize_min: 64
          machdep.cpu.mwait.linesize_max: 64
          machdep.cpu.mwait.extensions: 3
          machdep.cpu.mwait.sub_Cstates: 286531872
          machdep.cpu.thermal.sensor: 1
          machdep.cpu.thermal.dynamic_acceleration: 1
          machdep.cpu.thermal.invariant_APIC_timer: 1
          machdep.cpu.thermal.thresholds: 2
          machdep.cpu.thermal.ACNT_MCNT: 1
          machdep.cpu.thermal.core_power_limits: 1
          machdep.cpu.thermal.fine_grain_clock_mod: 1
          machdep.cpu.thermal.package_thermal_intr: 1
          machdep.cpu.thermal.hardware_feedback: 0
          machdep.cpu.thermal.energy_policy: 1
          machdep.cpu.xsave.extended_state: 31 832 1088 0
          machdep.cpu.xsave.extended_state1: 15 832 256 0
          machdep.cpu.arch_perf.version: 4
          machdep.cpu.arch_perf.number: 4
          machdep.cpu.arch_perf.width: 48
          machdep.cpu.arch_perf.events_number: 7
          machdep.cpu.arch_perf.events: 0
          machdep.cpu.arch_perf.fixed_number: 3
          machdep.cpu.arch_perf.fixed_width: 48
          machdep.cpu.cache.linesize: 64
          machdep.cpu.cache.L2_associativity: 4
          machdep.cpu.cache.size: 256
          machdep.cpu.tlb.inst.large: 8
          machdep.cpu.tlb.data.small: 64
          machdep.cpu.tlb.data.small_level1: 64
          


        1. 0xd34df00d
          20.06.2018 19:16
          +1

          Либо тайминги не те, если ошибка чувствительна к таймингам (но тогда вы её не изолируете со стопроцентной уверенностью, наверное, никогда).


      1. fedor1210
        20.06.2018 12:30

        Для запуска EFI-бинарников (а для Linux это понадобится), мне на современных Mac (после 2011) понадобился rEFInd, так как Boot Menu (которое появляется при удерживании клавиши Option при запуске, до появления яблока) никак не видело Debian. Попробуйте, конечно, развернуть на FAT32-флешку какой-нибудь дистрибутив, причем будет достаточно просто скопировать файлы, так как EFI не требует загрузочных секторов. Только проверьте в терминале с помощью

        diskutil list
        , чтобы на флешке не было EFI(ESP)-раздела, ибо он, похоже, и мешает стандартному Boot Menu «увидеть» устройство. Потому воспользуйтесь rEFInd, он у меня Debian обнаруживал всегда.


  1. dmytrobogovych
    19.06.2018 23:10

    Интересно, сравнение с учетом возможной погрешности помогает в этом случае?


  1. avdx
    19.06.2018 23:31

    Вообще похоже на ошибку процессора.
    А можно вывести результат приведения float к double, когда происходит ошибка?
    Т.е. вместо:
    float result = 1.0;

    if (result != 1.0) {
    error++;
    ...
    }

    Написать что то вроде:
    float result = 1.0;
    double resultd = result;

    if (resultd != 1.0) {
    error++;
    std::cout << "(double)1.0f = " << resultd << std::endl;
    ...
    }


    1. fls_welvet Автор
      19.06.2018 23:43

      Конечно. Результаты:
      Сравнение дабла: yadi.sk/i/1z-WK0H03Y8ota
      Сравнение флоата: yadi.sk/i/bnn8n6tm3Y8oyk


  1. avdx
    19.06.2018 23:59

    Выглядит все как ошибка железа при выполнении команды cvtss2sd.
    А можно еще проверить, точно там 0.0 получается. Может cout просто выводит денормализованные значения как 0.
    Можно заменить:
    std::cout << "(double)1.0f = " << resultd << std::endl;
    на
    std::cout << "(double)1.0f = " << *(long long*)&resultd << std::endl;


    1. fls_welvet Автор
      20.06.2018 00:03

      Похоже что там действительно 0:
      yadi.sk/i/nVpyIvDJ3Y8qzs


      1. avdx
        20.06.2018 00:12

        В любом случае других разумных объяснений, кроме ошибки процессора не вижу.
        А другие приложения, использующие floating point, не глючат на этой машине?


        1. fls_welvet Автор
          20.06.2018 00:24

          Глючат, но на удивление очень редко. Иногда bazel падает с тем же аут-оф-баунд при запуске. Иногда комп полностью зависает при сворачивании окна в док (в тот момент когда оно анимировано «уплывает»). Больше никаких проблем не замечал.

          Еще интересно — по совету из комментов выше пробовал запускать тест внутри докер контейнера на убунту и ошибка не повторяется (хотя инструкции там те же).


          1. avdx
            20.06.2018 11:05

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


            1. fls_welvet Автор
              20.06.2018 12:00

              Я пробовал грузить сейфмод с последующим отключением всего что не нужно. Кол-во ошибочных выполнений упало до 2х-3х на INT_MAX выборке. Пока не знаю как занизить частоту на маке — попробую разобраться вечером.


  1. port443
    20.06.2018 00:35

    Прошу прощения, я не уловил где именно делается сравнение float с 1.0?
    Немного удивляет такое, так как прямое сравнение чисел с плавающей точкой — один из древнейших антипаттернов.


    1. dordzhiev
      20.06.2018 01:22

      Делается в minimal repro на C++. Сравнение делается с константой 1.0, это не результат какого-то выражения, никаких проблемы быть не должно.


      1. port443
        20.06.2018 01:37

        eq/neq на float/double — это очень ненадёжная операция; там, где вы делаете сравнение, вполне может быть сравнение float (переменная) и double (константа) — как компилятор решит. Тем не менее, повторяемость должна быть 100%, согласен.


  1. theonlymirage
    20.06.2018 01:33

    Небольшая заметочка по теме…
    К сожалению, у меня на i7 (предыдущего поколения) с build 1.8.0_144 баг не повторяется (ни со строкой, ни с последними примерами). Но и java без изменения не даёт собрать ваш код: по умолчанию все дробные числа имеют тип double, поэтому строчка из последних примеров

    float result = 1.0;
    собирается только с явным указанием типа (без преобразования самим компилятором, иначе ошибка: «incompatible types: possible lossy conversion from double to float»). То есть так:
    float result = 1.0f;
    Влияет ли это как-то на генерацию дальнейшего асм-кода ещё не смотрел.
    P.S. Об этом даже в вики сказано и есть аналогичные примеры:
    float pi = 3.14f; // При использовании типа float требуется указывать суффикс f или F
    float anotherPi = (float) 3.14; // Можно привести явно


    1. Impuls
      20.06.2018 08:20

      Если вы про этот код:

      bool compare() {
          float val = 1.0;
          return val != 1.0;
      }

      То автор его на с++ написал. Это не java


      1. theonlymirage
        20.06.2018 14:35

        Да, но в с++ будет таже проблема. Эти строки делают неявное преобразование double во float. Проверьте ещё такой код:

        int a = 16777217 * 1.0f; 
        int b = 16777217 * 1.0;
        printf("%d %d\n", a, b); //16777216 16777217


        1. sasha1024
          20.06.2018 15:16

          Я не знаю (не смотрел сгенерированный ассемблерный код), но я думаю, что вряд ли строка

          float val = 1.0;
          
          будет делать преобразование double во float. Мне тоже первой пришла в голову мысль, что надо было бы = 1f, но скорее всего компилятор и так возьмёт 1f, а не будет делать явное преобразование double во float.

          Хотя для удобочитаемости — да, стоило б:
          bool compare() {
              float val = 1.0f;
              return val != 1.0;
          }
          


          1. port443
            20.06.2018 16:40

            Вы неправильно поняли предложенное.

            return val != 1.0f;

            Но в принципе такое в коде — уже плохо.


            1. sasha1024
              20.06.2018 16:53

              Так это ж бред тогда будет — тогда не будет cast'а к double — а эффект, как я понимаю, возникает как раз при наличии cast'а к double (cvtss2sd).

              Я бы наоборот, явно это выделил (забыл это сделать в прошлом комменте; хотя, возможно, автор специально так не сделал, потому что тогда какие-то другие эффекты всплывают?):

              bool compare() {
                  float val = 1.0f;
                  return (double)val != 1.0;
              }
              


  1. PlusPlus
    20.06.2018 09:13

    Надо искать на системе third-part kernel mode драйвер, который не сохраняет контекст перед использованием SSE или других FPU инструкций


  1. SopaXT
    20.06.2018 09:38

    А не связано ли это с CVE-2018-3665 и её патчем?


    1. fls_welvet Автор
      20.06.2018 09:47

      Спасибо за наводку. Тут пишут что «macOS before 10.13.5 is affected» (не знаю насколько это серьезный источник). Вчера обновился до System Version: macOS 10.13.5 (17F77) но проблема, к сожалению, не исчезла.


      1. aol-nnov
        20.06.2018 14:17

        не воспроизводится.

        System Version: macOS 10.13.3 (17D47)
        Kernel Version: Darwin 17.4.0
        machdep.cpu.brand_string: Intel® Core(TM) i7-4770HQ CPU @ 2.20GHz

        Apple LLVM version 9.0.0 (clang-900.0.39.2)


  1. vesper-bot
    20.06.2018 09:57

    Говорила мне мама, никогда не сравнивай числа с плавающей запятой! Проблема стара как мир формат float, преобразование float<->double не гарантирует равенство результатов, тем более если FPU до сих пор имеют 80-битные регистры от 80387. Я на эту проблему ещё в 1994м что ли нарвался, на Turbo Pascal 6.0, когда var twofloat:single, twodouble:double, twoext:extended присваивались значения 2.0 и потом с ним же сравнивались. Правда, в паскале все числа приводились к extended, а не к double, но смысл тот же. А ошибка тут в дизайне — нафига «expansionFactor» типа float? Если уж надо использовать вещественные числа, используйте нормальный тип, а не легаси. Тем более в случае, когда любое вещественное должно точно сводиться к дроби со знаменателем 8, правильнее использовать константу типа int, равную текущему expansionFactor, умноженному на 8.


    1. sasha1024
      20.06.2018 10:47
      +1

      Речь ведь совсем не о (float)3 != (double)3. А о том, что (int)(double)f, где float f = 3 регулярно возвращает не 3 (и даже не 2, что ещё хоть как-то можно было бы списать на неточность), а 0.


      1. vesper-bot
        20.06.2018 12:24

        Да уж, ноль такое преобразование вернуть вообще было не должно. А как по мне, задействовать здесь FPU вообще лишняя работа, с точки зрения исходных данных и результата. Но имеем что имеем.

        И в таком случае действительно вариант занизить частоту, может быть такое, что какой-то процесс некорректно отрабатывает на предельных частотах в этом конкретном «камне», например, не выставляется верхний бит экспоненты double в регистре xmm0, из-за чего число в регистре становится «почти нулем», и потом округляется в ноль. Это могло бы объяснить, почему float -> int проходит без проблем — тот бит не задействуется при таком преобразовании (да и вроде бы код float->int использует xmm1, а не xmm0). Возможно, удастся отловить проблему, например, записью в xmm0 статического значения 3.0 из памяти, потом чтение его в два разных места в памяти и побитового сравнения прочитанных результатов между собой и с исходным значением.


        1. vesper-bot
          20.06.2018 12:53
          +2

          в регистре xmm0

          какого-то конкретного ядра. Кстати, есть ли возможность в макоси выставить affinity процессу на конкретное ядро? Можно было бы изолировать проблемное ядро таким образом.


  1. sand14
    20.06.2018 13:52

    Если, как тут пишут проблема в том, что (int)(double)(float)3 возвращает 0, то это, конечно, баг на каком то уровне — процессор или другой чип, драйвер, ОС, etc.


    Однако, как уже отметили вопрос тут прежде всего по применимости float чисел к данной задаче.


    Дело не в том, что float числа нужно сравнивать с применением epsilon, а в более фундаментальной вещи: зачем вообще в стандартной библиотеке делать реализацию метода, с использованием float, который работает с вещами, которые к float вообще не имеют отношения? Это похоже на хак с использованием каких-то особенностей float, который в редких кейсах приводит к неожиданным проблемам.


    С float нужно работать только там, где входные и/или выходные данные float.


    В .NET тоже есть похожие реализации с использованием float там, где можно обойтись целочисленными типами (DateTime, TimeSpan — внутри хранят количество тиков в long, часть операций реализована как сложение/вычитание long, а часть — через операции с double).


    1. khrundel
      20.06.2018 15:09

      Видимо потому что множитель дробный. Можно обойтись fixed-point представлением, но это потребует умножения в широких интах, можно эмулировать через сдвиг и сложение, но зачем это всё, если есть флоаты? Только потому что у кого-то железо может глючить?


      1. sand14
        20.06.2018 15:19

        Видимо потому что множитель дробный.

        Понадобился дробный множитель, и, естественно, для этого было выбран тип данных дробных чисел, а, точнее — тип данных с плавающей точкой?


        Суть вопроса то в этом и заключается — зачем здесь дробный множитель, если есть в памяти строка из последовательности символов (UTF-16 или UTF-8 для java.lang.String? — кажется, первая), представленных целочисленными байтами, и нужно эти байты перекодировать так, чтобы они представляли ту же самую строку, но уже в другой кодировке?


        1. mayorovp
          20.06.2018 15:42
          +2

          Судя по всему, автор кода пытался таким образом уберечься от переполнения.


          Выражение 2000000000*3 даст в результате 1705032704, в то время как (int)(2000000000*3.0) даст 2147483647...


          1. theonlymirage
            20.06.2018 16:33

            Кажется найден первоисточник.
            Выше я привёл хороший пример и даже угадал, вот наш автор. Он пытался решить проблему java.nio.BufferOverflowException при вызове getBytes(), когда строка имеет определенную длину (от 16777216 символов).
            Есть ещё кое-что интересное, но не об этом.


          1. sand14
            20.06.2018 18:11

            Но это читерство, в реальном продуктовом и тем более библиотечном коде не должно быть:
            «2000000000 не можем умножить на 3», т.к. в результате «String.getBytes() does not work on some strings larger than 16MB», давайте тогда приведем все это к double перед умножением.
            Сорри, но это уровень студенческой лабораторной работы на тройку.

            Уж лучше тогда использовать BigIngteger, хотя это оверхед, да и на момент выявления бага BigInteger в Java еще не было.
            Если нужен был Workaround, то можно было б какой то свой Int128 (обертку над парой long) реализовать…
            Во всяком случае, если посмотрим исходники JDK 8 по Unsigned-арифметике, там BigInteger вполне используется для борьбы с переполнением.

            И наверняка можно было бы все решить проще и элегантнее — только не говорите, что надо было «быстро» — это ж библиотечная функция JDK, а не энтерпрайз-фичи по аджайлу и скраму.


            1. avdx
              20.06.2018 18:35
              +2

              Корень проблемы в том, что maxCharsPerByte() возвращает float. Почему так сделано я не знаю, но видимо это не обсуждается. В результате при умножении результата maxCharsPerByte() на int происходит умножение int на float. При арифметической операции с операндами разного типа оба операнда приводятся к тому типу, который может представить больший диапазон чисел, в данном случае это float. Но т.к. у float разрядность мантиссы 24 бита, а у int разрядность 32 бита, при привидении int к float сохраняются только 24 старших значащих бита, а младшие обнуляются, также если результат умножения получается больше 2^24, то его младшие биты тоже теряются. В итоге результат умножения получается меньше, чем должен быть в точной арифметике.
              На мой взгляд нормальным исправлением должно бы было быть изменение возвращаемого типа maxCharsPerByte() на целый, но видимо это невозможно по каким то причинам, поэтому приведение к double вполне корректное решение.


              1. sand14
                20.06.2018 19:24

                > Корень проблемы в том, что maxCharsPerByte() возвращает float

                Тогда совсем печально.
                Но позитив тут в том, что тут пример того, почему контракты библиотечных функций должны тщательно продумываться — ибо это влияет не только на продуктовый код, но на на другие библиотечные функции, и просто так это уже не вычистишь, пометив устаревшую функцию как deprecated.


  1. f1inx
    20.06.2018 16:28

    Скоре всего проблема в latency результатов выполнения SSE команд, возможно под нагрузкой она может стать внезапно достаточно большой и в %eax будет сидеть не то что ожидается (в результате vcvttsd2si %eax,%xmm0 согласно тестам latency бывает >12тактов хотя дока утверждает 8), что вполне может быть не документировано. Такого рода «ошибки достаточно часто встречаются в intel CPU и nvidia GPU.Я бы рекомендовал попробовать встроить между этой инструкцией и сравнением %eax один или более NOP. Конечно проблема с конкретным CPU тоже не исключена.


  1. randomizer2222
    20.06.2018 17:10

    MacBook Pro (15-inch, 2017)
    2.8 GHz Intel Core i7
    OS: 10.13.4 (17E202)
    Memory: 16 GB 2133 MHz LPDDR3
    Проблема не воспроизводится. Ни на C++ ни Java.

    Проблема может быть конкретно в вашем ноутбуке.
    Но проблема весьма занятна. Обычно если 0.1+0.2 != 0.3 то оно всегда так и не меняется от количества вызовов.

    Жду продолжения, если найдете причину.


    1. sasha1024
      20.06.2018 17:18

      Интересно, существуют ли тесты для процессора (которые user мог бы запустить и проверить, не дефектный ли его процессор)?
      И программные способы залочить отдельные ядра (вдруг проблема в каком-то одном из)?


      1. Germanets
        20.06.2018 17:24
        +2

        Как минимум есть The Intel® Processor Diagnostic Tool