Привет, Хабр!
Все началось с того что я писал 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)
kemsky
19.06.2018 18:46Интересная проблема, попробуйте с 'strictfp' в Java или с аналогичными опциями clang. Смущает только, что слишком большая разница, как для ошибки округления, я бы продолжил поиски)
fls_welvet Автор
19.06.2018 19:03Честно говоря не знаю куда дальше продолжать :)
Добавление strictfp к бенчмарк классу в Java ошибку не убрало. Не уверен как это можно включить для clang. В интернете я нашел что можно проверить std::numeric_limits::is_iec559 — «возвращает» 1.
vladimir_dolzhenko
19.06.2018 19:19Интересно какая версия java? Кто вендор? Спрашиваю из соображений проверить на своём макбуке
fls_welvet Автор
19.06.2018 19:23Версия HotSpot java version «1.8.0_171».
У коллег проблема не повторялась. Исходя из того что проблема появлялась очень редко и воспроизвелась на простом cpp методе, я думаю что дело именно в железе конкретного компьютера.
kibb
19.06.2018 19:47+2Я бы начал с того, что обновил операционку. Такая проблема скорее похожа на ошибку сохранения/восстановления контекста FPU, а не аппаратную. Кроме того, поскольку у вас есть изолированный пример на С++, попробуйте загрузить live-CD с линуксом и проверьте на этом же железе, но с другой OS.
fls_welvet Автор
19.06.2018 20:39Спасибо за совет.
— Обновить ОС не помогло.
— Я попробовал запустить докер контейнер с убунту и собрать бинарник там. Внутри контейнера ошибка не повторилась! (проверил gdb что sse инструкции используются). Проверить на реальном live cd пока нет возможности так как на ноутбуке только usb-c порты :( Проверю на «чистом» линуксе как появится возможность.vassabi
20.06.2018 02:18Внутри контейнера ошибка не повторилась! (проверил gdb что sse инструкции используются).
если не повторилась — значит точно не железо, а операционка.
Странно только почему (т.е. какая версия+билд ОС и какая версия фирмвари в ЦПУ)…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
0xd34df00d
20.06.2018 19:16+1Либо тайминги не те, если ошибка чувствительна к таймингам (но тогда вы её не изолируете со стопроцентной уверенностью, наверное, никогда).
fedor1210
20.06.2018 12:30Для запуска EFI-бинарников (а для Linux это понадобится), мне на современных Mac (после 2011) понадобился rEFInd, так как Boot Menu (которое появляется при удерживании клавиши Option при запуске, до появления яблока) никак не видело Debian. Попробуйте, конечно, развернуть на FAT32-флешку какой-нибудь дистрибутив, причем будет достаточно просто скопировать файлы, так как EFI не требует загрузочных секторов. Только проверьте в терминале с помощью
, чтобы на флешке не было EFI(ESP)-раздела, ибо он, похоже, и мешает стандартному Boot Menu «увидеть» устройство. Потому воспользуйтесь rEFInd, он у меня Debian обнаруживал всегда.diskutil list
dmytrobogovych
19.06.2018 23:10Интересно, сравнение с учетом возможной погрешности помогает в этом случае?
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;
...
}
fls_welvet Автор
19.06.2018 23:43Конечно. Результаты:
Сравнение дабла: yadi.sk/i/1z-WK0H03Y8ota
Сравнение флоата: yadi.sk/i/bnn8n6tm3Y8oyk
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;
fls_welvet Автор
20.06.2018 00:03Похоже что там действительно 0:
yadi.sk/i/nVpyIvDJ3Y8qzsavdx
20.06.2018 00:12В любом случае других разумных объяснений, кроме ошибки процессора не вижу.
А другие приложения, использующие floating point, не глючат на этой машине?fls_welvet Автор
20.06.2018 00:24Глючат, но на удивление очень редко. Иногда bazel падает с тем же аут-оф-баунд при запуске. Иногда комп полностью зависает при сворачивании окна в док (в тот момент когда оно анимировано «уплывает»). Больше никаких проблем не замечал.
Еще интересно — по совету из комментов выше пробовал запускать тест внутри докер контейнера на убунту и ошибка не повторяется (хотя инструкции там те же).avdx
20.06.2018 11:05Видимо проблема проявляется только под нагрузкой, возможно когда из-за этого процессор повышает тактовую частоту. Я думаю стоит попробовать ограничить или уменьшить максимальную частоту процессора и посмотреть будет ли повторяться проблема.
fls_welvet Автор
20.06.2018 12:00Я пробовал грузить сейфмод с последующим отключением всего что не нужно. Кол-во ошибочных выполнений упало до 2х-3х на INT_MAX выборке. Пока не знаю как занизить частоту на маке — попробую разобраться вечером.
port443
20.06.2018 00:35Прошу прощения, я не уловил где именно делается сравнение float с 1.0?
Немного удивляет такое, так как прямое сравнение чисел с плавающей точкой — один из древнейших антипаттернов.dordzhiev
20.06.2018 01:22Делается в minimal repro на C++. Сравнение делается с константой 1.0, это не результат какого-то выражения, никаких проблемы быть не должно.
port443
20.06.2018 01:37eq/neq на float/double — это очень ненадёжная операция; там, где вы делаете сравнение, вполне может быть сравнение float (переменная) и double (константа) — как компилятор решит. Тем не менее, повторяемость должна быть 100%, согласен.
theonlymirage
20.06.2018 01:33Небольшая заметочка по теме…
К сожалению, у меня на i7 (предыдущего поколения) с build 1.8.0_144 баг не повторяется (ни со строкой, ни с последними примерами). Но и java без изменения не даёт собрать ваш код: по умолчанию все дробные числа имеют тип double, поэтому строчка из последних примеров
собирается только с явным указанием типа (без преобразования самим компилятором, иначе ошибка: «incompatible types: possible lossy conversion from double to float»). То есть так:float result = 1.0;
Влияет ли это как-то на генерацию дальнейшего асм-кода ещё не смотрел.float result = 1.0f;
P.S. Об этом даже в вики сказано и есть аналогичные примеры:
float pi = 3.14f; // При использовании типа float требуется указывать суффикс f или F float anotherPi = (float) 3.14; // Можно привести явно
Impuls
20.06.2018 08:20Если вы про этот код:
bool compare() { float val = 1.0; return val != 1.0; }
То автор его на с++ написал. Это не javatheonlymirage
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
sasha1024
20.06.2018 15:16Я не знаю (не смотрел сгенерированный ассемблерный код), но я думаю, что вряд ли строка
будет делать преобразование double во float. Мне тоже первой пришла в голову мысль, что надо было быfloat val = 1.0;
= 1f
, но скорее всего компилятор и так возьмёт1f
, а не будет делать явное преобразование double во float.
Хотя для удобочитаемости — да, стоило б:bool compare() { float val = 1.0f; return val != 1.0; }
port443
20.06.2018 16:40Вы неправильно поняли предложенное.
return val != 1.0f;
Но в принципе такое в коде — уже плохо.sasha1024
20.06.2018 16:53Так это ж бред тогда будет — тогда не будет cast'а к double — а эффект, как я понимаю, возникает как раз при наличии cast'а к double (cvtss2sd).
Я бы наоборот, явно это выделил (забыл это сделать в прошлом комменте; хотя, возможно, автор специально так не сделал, потому что тогда какие-то другие эффекты всплывают?):bool compare() { float val = 1.0f; return (double)val != 1.0; }
PlusPlus
20.06.2018 09:13Надо искать на системе third-part kernel mode драйвер, который не сохраняет контекст перед использованием SSE или других FPU инструкций
SopaXT
20.06.2018 09:38А не связано ли это с CVE-2018-3665 и её патчем?
fls_welvet Автор
20.06.2018 09:47Спасибо за наводку. Тут пишут что «macOS before 10.13.5 is affected» (не знаю насколько это серьезный источник). Вчера обновился до System Version: macOS 10.13.5 (17F77) но проблема, к сожалению, не исчезла.
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)
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.sasha1024
20.06.2018 10:47+1Речь ведь совсем не о
(float)3 != (double)3
. А о том, что(int)(double)f
, гдеfloat f = 3
регулярно возвращает не 3 (и даже не 2, что ещё хоть как-то можно было бы списать на неточность), а 0.vesper-bot
20.06.2018 12:24Да уж, ноль такое преобразование вернуть вообще было не должно. А как по мне, задействовать здесь FPU вообще лишняя работа, с точки зрения исходных данных и результата. Но имеем что имеем.
И в таком случае действительно вариант занизить частоту, может быть такое, что какой-то процесс некорректно отрабатывает на предельных частотах в этом конкретном «камне», например, не выставляется верхний бит экспоненты double в регистре xmm0, из-за чего число в регистре становится «почти нулем», и потом округляется в ноль. Это могло бы объяснить, почему float -> int проходит без проблем — тот бит не задействуется при таком преобразовании (да и вроде бы код float->int использует xmm1, а не xmm0). Возможно, удастся отловить проблему, например, записью в xmm0 статического значения 3.0 из памяти, потом чтение его в два разных места в памяти и побитового сравнения прочитанных результатов между собой и с исходным значением.vesper-bot
20.06.2018 12:53+2в регистре xmm0
какого-то конкретного ядра. Кстати, есть ли возможность в макоси выставить affinity процессу на конкретное ядро? Можно было бы изолировать проблемное ядро таким образом.
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).
khrundel
20.06.2018 15:09Видимо потому что множитель дробный. Можно обойтись fixed-point представлением, но это потребует умножения в широких интах, можно эмулировать через сдвиг и сложение, но зачем это всё, если есть флоаты? Только потому что у кого-то железо может глючить?
sand14
20.06.2018 15:19Видимо потому что множитель дробный.
Понадобился дробный множитель, и, естественно, для этого было выбран тип данных дробных чисел, а, точнее — тип данных с плавающей точкой?
Суть вопроса то в этом и заключается — зачем здесь дробный множитель, если есть в памяти строка из последовательности символов (UTF-16 или UTF-8 для java.lang.String? — кажется, первая), представленных целочисленными байтами, и нужно эти байты перекодировать так, чтобы они представляли ту же самую строку, но уже в другой кодировке?
mayorovp
20.06.2018 15:42+2Судя по всему, автор кода пытался таким образом уберечься от переполнения.
Выражение
2000000000*3
даст в результате 1705032704, в то время как(int)(2000000000*3.0)
даст 2147483647...theonlymirage
20.06.2018 16:33Кажется найден первоисточник.
Выше я привёл хороший пример и даже угадал, вот наш автор. Он пытался решить проблему java.nio.BufferOverflowException при вызове getBytes(), когда строка имеет определенную длину (от 16777216 символов).
Есть ещё кое-что интересное, но не об этом.
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, а не энтерпрайз-фичи по аджайлу и скраму.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 вполне корректное решение.sand14
20.06.2018 19:24> Корень проблемы в том, что maxCharsPerByte() возвращает float
Тогда совсем печально.
Но позитив тут в том, что тут пример того, почему контракты библиотечных функций должны тщательно продумываться — ибо это влияет не только на продуктовый код, но на на другие библиотечные функции, и просто так это уже не вычистишь, пометив устаревшую функцию как deprecated.
f1inx
20.06.2018 16:28Скоре всего проблема в latency результатов выполнения SSE команд, возможно под нагрузкой она может стать внезапно достаточно большой и в %eax будет сидеть не то что ожидается (в результате vcvttsd2si %eax,%xmm0 согласно тестам latency бывает >12тактов хотя дока утверждает 8), что вполне может быть не документировано. Такого рода «ошибки достаточно часто встречаются в intel CPU и nvidia GPU.Я бы рекомендовал попробовать встроить между этой инструкцией и сравнением %eax один или более NOP. Конечно проблема с конкретным CPU тоже не исключена.
randomizer2222
20.06.2018 17:10MacBook 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 то оно всегда так и не меняется от количества вызовов.
Жду продолжения, если найдете причину.sasha1024
20.06.2018 17:18Интересно, существуют ли тесты для процессора (которые user мог бы запустить и проверить, не дефектный ли его процессор)?
И программные способы залочить отдельные ядра (вдруг проблема в каком-то одном из)?
Sabubu
Производителю процессора сообщили?
SergeyMax
Я бы начал с прогона мемтеста)
fls_welvet Автор
Спасибо за совет, попробую поставить built-in тест на ночь.
Garbus
И еще интересно, это проблема конкретного экземпляра процессора, партии и т.д.? Все же нынче некоторый процент брака стал нормой, вдруг просто проскочил при тестировании.