Как у меня сломался String.getBytes(UTF_8) и что я с этим делал

    (спойлер) дебажил, дизасемблил и пришел к выводу что проблема в 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'а в какой то момент может сломаться это конечно шок. И что с этим можно сделать кроме как заменить Мак не ясно.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 54

      0
      Производителю процессора сообщили?
        +6
        Я бы начал с прогона мемтеста)
          +1
          Спасибо за совет, попробую поставить built-in тест на ночь.
          +1
          И еще интересно, это проблема конкретного экземпляра процессора, партии и т.д.? Все же нынче некоторый процент брака стал нормой, вдруг просто проскочил при тестировании.
          0

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

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

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

              strictfp на современных процессорах ни на что не влияет. Его даже убрать хотят из джавы вообще.

              0
              Интересно какая версия java? Кто вендор? Спрашиваю из соображений проверить на своём макбуке
                0
                Версия HotSpot java version «1.8.0_171».
                У коллег проблема не повторялась. Исходя из того что проблема появлялась очень редко и воспроизвелась на простом cpp методе, я думаю что дело именно в железе конкретного компьютера.
                +5
                Я бы начал с того, что обновил операционку. Такая проблема скорее похожа на ошибку сохранения/восстановления контекста FPU, а не аппаратную. Кроме того, поскольку у вас есть изолированный пример на С++, попробуйте загрузить live-CD с линуксом и проверьте на этом же железе, но с другой OS.
                  0
                  Спасибо за совет.

                  — Обновить ОС не помогло.
                  — Я попробовал запустить докер контейнер с убунту и собрать бинарник там. Внутри контейнера ошибка не повторилась! (проверил gdb что sse инструкции используются). Проверить на реальном live cd пока нет возможности так как на ноутбуке только usb-c порты :( Проверю на «чистом» линуксе как появится возможность.
                    +1
                    Внутри контейнера ошибка не повторилась! (проверил gdb что sse инструкции используются).
                    если не повторилась — значит точно не железо, а операционка.
                    Странно только почему (т.е. какая версия+билд ОС и какая версия фирмвари в ЦПУ)…
                      +1
                      Версии
                      ➜  ~ 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
                        Либо тайминги не те, если ошибка чувствительна к таймингам (но тогда вы её не изолируете со стопроцентной уверенностью, наверное, никогда).
                        0
                        Для запуска EFI-бинарников (а для Linux это понадобится), мне на современных Mac (после 2011) понадобился rEFInd, так как Boot Menu (которое появляется при удерживании клавиши Option при запуске, до появления яблока) никак не видело Debian. Попробуйте, конечно, развернуть на FAT32-флешку какой-нибудь дистрибутив, причем будет достаточно просто скопировать файлы, так как EFI не требует загрузочных секторов. Только проверьте в терминале с помощью
                        diskutil list
                        , чтобы на флешке не было EFI(ESP)-раздела, ибо он, похоже, и мешает стандартному Boot Menu «увидеть» устройство. Потому воспользуйтесь rEFInd, он у меня Debian обнаруживал всегда.
                      0
                      Интересно, сравнение с учетом возможной погрешности помогает в этом случае?
                        0
                        Вообще похоже на ошибку процессора.
                        А можно вывести результат приведения 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;
                        ...
                        }

                        0
                        Выглядит все как ошибка железа при выполнении команды cvtss2sd.
                        А можно еще проверить, точно там 0.0 получается. Может cout просто выводит денормализованные значения как 0.
                        Можно заменить:
                        std::cout << "(double)1.0f = " << resultd << std::endl;
                        на
                        std::cout << "(double)1.0f = " << *(long long*)&resultd << std::endl;
                          0
                          Похоже что там действительно 0:
                          yadi.sk/i/nVpyIvDJ3Y8qzs
                            0
                            В любом случае других разумных объяснений, кроме ошибки процессора не вижу.
                            А другие приложения, использующие floating point, не глючат на этой машине?
                              0
                              Глючат, но на удивление очень редко. Иногда bazel падает с тем же аут-оф-баунд при запуске. Иногда комп полностью зависает при сворачивании окна в док (в тот момент когда оно анимировано «уплывает»). Больше никаких проблем не замечал.

                              Еще интересно — по совету из комментов выше пробовал запускать тест внутри докер контейнера на убунту и ошибка не повторяется (хотя инструкции там те же).
                                0
                                Видимо проблема проявляется только под нагрузкой, возможно когда из-за этого процессор повышает тактовую частоту. Я думаю стоит попробовать ограничить или уменьшить максимальную частоту процессора и посмотреть будет ли повторяться проблема.
                                  0
                                  Я пробовал грузить сейфмод с последующим отключением всего что не нужно. Кол-во ошибочных выполнений упало до 2х-3х на INT_MAX выборке. Пока не знаю как занизить частоту на маке — попробую разобраться вечером.
                          0
                          Прошу прощения, я не уловил где именно делается сравнение float с 1.0?
                          Немного удивляет такое, так как прямое сравнение чисел с плавающей точкой — один из древнейших антипаттернов.
                            0
                            Делается в minimal repro на C++. Сравнение делается с константой 1.0, это не результат какого-то выражения, никаких проблемы быть не должно.
                              –5
                              eq/neq на float/double — это очень ненадёжная операция; там, где вы делаете сравнение, вполне может быть сравнение float (переменная) и double (константа) — как компилятор решит. Тем не менее, повторяемость должна быть 100%, согласен.
                            0
                            Небольшая заметочка по теме…
                            К сожалению, у меня на 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; // Можно привести явно
                              +2
                              Если вы про этот код:
                              bool compare() {
                                  float val = 1.0;
                                  return val != 1.0;
                              }

                              То автор его на с++ написал. Это не java
                                0
                                Да, но в с++ будет таже проблема. Эти строки делают неявное преобразование double во float. Проверьте ещё такой код:
                                int a = 16777217 * 1.0f; 
                                int b = 16777217 * 1.0;
                                printf("%d %d\n", a, b); //16777216 16777217
                                  0
                                  Я не знаю (не смотрел сгенерированный ассемблерный код), но я думаю, что вряд ли строка
                                  float val = 1.0;
                                  
                                  будет делать преобразование double во float. Мне тоже первой пришла в голову мысль, что надо было бы = 1f, но скорее всего компилятор и так возьмёт 1f, а не будет делать явное преобразование double во float.

                                  Хотя для удобочитаемости — да, стоило б:
                                  bool compare() {
                                      float val = 1.0f;
                                      return val != 1.0;
                                  }
                                  
                                    0
                                    Вы неправильно поняли предложенное.
                                    return val != 1.0f;

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

                                      Я бы наоборот, явно это выделил (забыл это сделать в прошлом комменте; хотя, возможно, автор специально так не сделал, потому что тогда какие-то другие эффекты всплывают?):
                                      bool compare() {
                                          float val = 1.0f;
                                          return (double)val != 1.0;
                                      }
                                      
                              +2
                              Надо искать на системе third-part kernel mode драйвер, который не сохраняет контекст перед использованием SSE или других FPU инструкций
                                0

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

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

                                    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)
                                  –3
                                  Говорила мне мама, никогда не сравнивай числа с плавающей запятой! Проблема стара как мир формат 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.
                                    +8
                                    Речь ведь совсем не о (float)3 != (double)3. А о том, что (int)(double)f, где float f = 3 регулярно возвращает не 3 (и даже не 2, что ещё хоть как-то можно было бы списать на неточность), а 0.
                                      +1
                                      Да уж, ноль такое преобразование вернуть вообще было не должно. А как по мне, задействовать здесь FPU вообще лишняя работа, с точки зрения исходных данных и результата. Но имеем что имеем.

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

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

                                    –2

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


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


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


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


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

                                      –1

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

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

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


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

                                          +3

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


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

                                            0
                                            Кажется найден первоисточник.
                                            Выше я привёл хороший пример и даже угадал, вот наш автор. Он пытался решить проблему java.nio.BufferOverflowException при вызове getBytes(), когда строка имеет определенную длину (от 16777216 символов).
                                            Есть ещё кое-что интересное, но не об этом.
                                              +1
                                              Но это читерство, в реальном продуктовом и тем более библиотечном коде не должно быть:
                                              «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, а не энтерпрайз-фичи по аджайлу и скраму.
                                                +2
                                                Корень проблемы в том, что maxCharsPerByte() возвращает float. Почему так сделано я не знаю, но видимо это не обсуждается. В результате при умножении результата maxCharsPerByte() на int происходит умножение int на float. При арифметической операции с операндами разного типа оба операнда приводятся к тому типу, который может представить больший диапазон чисел, в данном случае это float. Но т.к. у float разрядность мантиссы 24 бита, а у int разрядность 32 бита, при привидении int к float сохраняются только 24 старших значащих бита, а младшие обнуляются, также если результат умножения получается больше 2^24, то его младшие биты тоже теряются. В итоге результат умножения получается меньше, чем должен быть в точной арифметике.
                                                На мой взгляд нормальным исправлением должно бы было быть изменение возвращаемого типа maxCharsPerByte() на целый, но видимо это невозможно по каким то причинам, поэтому приведение к double вполне корректное решение.
                                                  +1
                                                  > Корень проблемы в том, что maxCharsPerByte() возвращает float

                                                  Тогда совсем печально.
                                                  Но позитив тут в том, что тут пример того, почему контракты библиотечных функций должны тщательно продумываться — ибо это влияет не только на продуктовый код, но на на другие библиотечные функции, и просто так это уже не вычистишь, пометив устаревшую функцию как deprecated.
                                        –2
                                        Скоре всего проблема в latency результатов выполнения SSE команд, возможно под нагрузкой она может стать внезапно достаточно большой и в %eax будет сидеть не то что ожидается (в результате vcvttsd2si %eax,%xmm0 согласно тестам latency бывает >12тактов хотя дока утверждает 8), что вполне может быть не документировано. Такого рода «ошибки достаточно часто встречаются в intel CPU и nvidia GPU.Я бы рекомендовал попробовать встроить между этой инструкцией и сравнением %eax один или более NOP. Конечно проблема с конкретным CPU тоже не исключена.
                                          0
                                          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 то оно всегда так и не меняется от количества вызовов.

                                          Жду продолжения, если найдете причину.
                                            0
                                            Интересно, существуют ли тесты для процессора (которые user мог бы запустить и проверить, не дефектный ли его процессор)?
                                            И программные способы залочить отдельные ядра (вдруг проблема в каком-то одном из)?
                                          +2
                                          Читал недавно похожую историю, но которая затрагивает все CPU. Тоже сильно удивился. Так что да, примитивы не всегда так уж надежны.

                                          Only users with full accounts can post comments. Log in, please.