Ошибки вычисления значения двойной точности на процессорах MediaTek

Я обнаружил, что одно из моих приложений, опубликованных на рынке, производит странные результаты на некоторых телефонах. При исследовании выясняется, что существует проблема с одной функцией, которая вычисляет расстояние между двумя GeoPoints – иногда она возвращает полностью неправильное значение. Этот вопрос воспроизводится только на устройствах с MediaTek MT6589 SoC (aka MTK6589). И AFAIK у всех таких устройств установлен Android 4.2.

Обновление. Я также смог воспроизвести ошибку на планшете Lenovo S6000 с чипом MediaTek MT8125 / 8389 и на Fly IQ444 Quattro с MT6589 и с установленным Android 4.1 .

Я создал тестовый проект, который помогает воспроизвести ошибку. Он многократно выполняет вычисления для 1'000 или 100'000 итераций. Чтобы исключить возможность вычисления потоков, вычисление выполняется в потоке пользовательского интерфейса (с небольшими паузами, чтобы поддерживать пользовательский интерфейс). В тестовом проекте я использовал только часть исходной формулы расстояния:

private double calcX() { double t = 1.0; double X = 0.5 + t / 16384; return X; } 

Как вы можете проверить самостоятельно на web2.0calc.com, значение X должно быть приблизительно: 0.50006103515625 .
Однако на устройствах с микросхемой MT6589 часто вычисляется неправильное значение: 2.0 .

Проект доступен в Google Code (также доступна APK ). Источник тестового класса представлен ниже:

 public class MtkTestActivity extends Activity { static final double A = 0.5; static final double B = 1; static final double D = 16384; static final double COMPUTED_CONST = A + B / D; /* * Main calculation where bug occurs */ public double calcX() { double t = B; double X = A + t / D; return X; } class TestRunnable implements Runnable { static final double EP = 0.00000000001; static final double EXPECTED_LOW = COMPUTED_CONST - EP; static final double EXPECTED_HIGH = COMPUTED_CONST + EP; public void run() { for (int i = 0; i < SMALL_ITERATION; i++) { double A = calcX(); if (A < EXPECTED_LOW || A > EXPECTED_HIGH) { mFailedInCycle = true; mFails++; mEdit.getText().append("FAILED on " + mIteration + " iteration with: " + A + '\n'); } mIteration++; } if (mIteration % 5000 == 0) { if (mFailedInCycle) { mFailedInCycle = false; } else { mEdit.getText().append("passed " + mIteration + " iterations\n"); } } if (mIteration < mIterationsCount) { mHandler.postDelayed(new TestRunnable(), DELAY); } else { mEdit.getText().append("\nFinished test with " + mFails + " fails"); } } } public void onTestClick(View v) { startTest(IT_10K); } public void onTestClick100(View v) { startTest(IT_100K); } private void startTest(int iterationsCount) { Editable text = mEdit.getText(); text.clear(); text.append("\nStarting " + iterationsCount + " iterations test..."); text.append("\n\nExpected result " + COMPUTED_CONST + "\n\n"); mIteration = 0; mFails = 0; mFailedInCycle = false; mIterationsCount = iterationsCount; mHandler.postDelayed(new TestRunnable(), 100); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mHandler = new Handler(getMainLooper()); mEdit = (EditText) findViewById(R.id.edtText1); } private static final int IT_10K = 1000; private static final int IT_100K = 100000; private static final int SMALL_ITERATION = 50; private static final int DELAY = 10; private int mIteration; private int mFails; private boolean mFailedInCycle; private Handler mHandler; private int mIterationsCount; private EditText mEdit; } 

Чтобы исправить проблему, достаточно просто изменить все double для float в calcX() .

Дальнейшее расследование
Отключение JIT (добавление android:vmSafeMode="true" в манифест приложения) также исправляет ошибку.

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

Ps: если кто-нибудь сможет воспроизвести эту ошибку на устройстве с другим чипом или может протестировать ее с помощью любого чипа MediaTek и Android> = 4.3, я буду очень этому признателен.

Solutions Collecting From Web of "Ошибки вычисления значения двойной точности на процессорах MediaTek"

Это была ошибка JIT, которая была активной в источнике JellyBean с конца 2012 года по начало 2013 года. Короче говоря, если две или более константы с двойной точностью, которые были разными в 32 разрядах, но одинаковые в младших 32 битах, были использованы в Тот же базовый блок JIT будет думать, что они были одинаковыми, и ненадлежащим образом оптимизировать один из них.

Я ввел дефект в: https://android-review.googlesource.com/#/c/47280/

И зафиксировал его в: https://android-review.googlesource.com/#/c/57602/

Дефект не должен появляться в каких-либо последних версиях Android.

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

Они появляются иногда на нескольких списках рассылки Android.

Я считаю, что то, что вы видите, это эффект (1) различных процессоров и их обработка значений с плавающей запятой и (2) различия в размерах хранилища, которые приводят к различным округлениям и усечениям.

Для (1) используйте в своем коде что-то вроде следующего:

  • _controlfp(_PC_24, _MCW_PC);
  • _controlfp(_RC_NEAR, _MCW_RC);

Для (2) используйте общий размер хранилища, который является float .

Иногда в родном мире иногда возникает другая проблема: float передается функции, но значение в функции всегда 0.0f (вместо значения non-0, используемого для вызова функции). Вы можете очистить это с помощью -mfloat-abi=softfp . См. Hard-float и JNI .

К сожалению, вы находитесь во власти производителя при использовании своего порта Android Java. Наслаждайтесь их настройками, надзорами и ошибками реализации. По крайней мере, это не повредит вашу виртуальную машину .

Я провел последнюю неделю, исследуя эту проблему, и вот что я нашел:

  • Эта ошибка ранее была замечена пользователями устройств MT6589 (например, здесь и здесь )
  • Общим способом было отключить JIT (для конкретного приложения или для всей системы)
  • Я смог воспроизвести этот вопрос на двух устройствах с MT6589 и MT8125 / 8389, в настоящее время он не был воспроизведен на устройствах с чипами, кроме упомянутых, см. Разделы обновления ниже
  • Выражение, которое воспроизводит ошибку, если намного проще, чем я опубликовал в вопросе, просто:
    X = A + b / D
  • Задержка между вычислениями является важной частью ошибки: без нее ошибка появляется спорадически, с небольшим сном после вычисления она воспроизводит всегда (как только код был JITed)
  • Я создал скрипт, который собирает простой файл jar и запускает dalvikvm напрямую, передавая ему параметры. Это позволило установить jit-порог и получить выходной ARM-код, сгенерированный JIT
  • Передача -Xjitdisableopt:1 в Dalvik исправляет проблему (этот параметр отключает оптимизацию kLoadStoreElimination ). Можно также добавить dalvik.vm.extra-opts=-Xjitdisableopt:1 в файл build.prop как быстрое обходное решение, которое сохраняет JIT (требуется root и перезагрузка)
  • Хотя эта проблема похожа на ошибку # 63790, упомянутую Скоттом Бартой , я думаю, что она другая (также автор упомянутой ошибки Дмитрий подтвердил, что эта ошибка «Mediatek» не воспроизводится на телефоне, затронутом # 63790)
  • ОБНОВЛЕНИЕ: я libdvm.so (из Fly IQ4410 с чипом MT6589) в эмулятор и там воспроизвел ошибку. Но если я использую libdvm.so скомпилированный из источников Android 4.2, ошибка исчезает. Похоже, что существует проблема с JIT-скомпилированным кодом, созданным определенной версией библиотеки libdvm поставляемой с затронутыми устройствами
  • ОБНОВЛЕНИЕ: успешно воспроизведена ошибка на телефоне Samsung Ace 2 ( NovaThor U8500 , Android 4.1.2), используя ту же технику, что и выше, – взял libdvm.so от Fly IQ444 (MT6589, Android 4.1.2)

Я отправил отчет об ошибке № 65750 .

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

 public class Calc { static final double A = 0.5; static final double B = 1; static final double D = 16384; public double calcX() { double t = B; double X = A + t / D; return X; } } 

Выход JIT для обычного запуска Dalvik:

 D/dalvikvm: Dumping LIR insns D/dalvikvm: installed code is at 0x45deb000 D/dalvikvm: total size is 124 bytes D/dalvikvm: 0x45deb000 (0000): data 0xc278(49784) D/dalvikvm: 0x45deb002 (0002): data 0x457a(17786) D/dalvikvm: 0x45deb004 (0004): data 0x0044(68) D/dalvikvm: 0x45deb006 (0006): ldr r0, [r15pc, -#8] D/dalvikvm: 0x45deb00a (000a): ldr r1, [r0, #0] D/dalvikvm: 0x45deb00c (000c): adds r1, r1, #1 D/dalvikvm: 0x45deb00e (000e): str r1, [r0, #0] D/dalvikvm: -------- entry offset: 0x0000 D/dalvikvm: L0x4579e28c: D/dalvikvm: -------- dalvik offset: 0x0000 @ const-wide/high16 v0, (#16368), (#0) D/dalvikvm: 0x45deb010 (0010): vldr d8, [r15, #96] D/dalvikvm: -------- dalvik offset: 0x0002 @ const-wide/high16 v2, (#16352), (#0) D/dalvikvm: 0x45deb014 (0014): vmov.f64 d9, d8 D/dalvikvm: -------- dalvik offset: 0x0004 @ const-wide/high16 v4, (#16592), (#0) D/dalvikvm: 0x45deb018 (0018): vmov.f64 d10, d9 D/dalvikvm: -------- dalvik offset: 0x0006 @ div-double/2addr v0, v4, (#0) D/dalvikvm: 0x45deb01c (001c): vdivd d8, d8, d10 D/dalvikvm: -------- dalvik offset: 0x0007 @ add-double/2addr v0, v2, (#0) D/dalvikvm: 0x45deb020 (0020): vadd d8, d8, d9 D/dalvikvm: -------- dalvik offset: 0x0008 @ return-wide v0, (#0), (#0) D/dalvikvm: 0x45deb024 (0024): vmov.f64 d11, d8 D/dalvikvm: 0x45deb028 (0028): vstr d11, [r6, #16] D/dalvikvm: 0x45deb02c (002c): vstr d8, [r5, #0] D/dalvikvm: 0x45deb030 (0030): vstr d10, [r5, #16] D/dalvikvm: 0x45deb034 (0034): vstr d9, [r5, #8] D/dalvikvm: 0x45deb038 (0038): blx_1 0x45dea028 D/dalvikvm: 0x45deb03a (003a): blx_2 see above D/dalvikvm: 0x45deb03c (003c): b 0x45deb040 (L0x4579f068) D/dalvikvm: 0x45deb03e (003e): undefined D/dalvikvm: L0x4579f068: D/dalvikvm: -------- reconstruct dalvik PC : 0x457b83f4 @ +0x0008 D/dalvikvm: 0x45deb040 (0040): ldr r0, [r15pc, #28] D/dalvikvm: Exception_Handling: D/dalvikvm: 0x45deb044 (0044): ldr r1, [r6, #108] D/dalvikvm: 0x45deb046 (0046): blx r1 D/dalvikvm: -------- end of chaining cells (0x0048) D/dalvikvm: 0x45deb060 (0060): .word (0x457b83f4) D/dalvikvm: 0x45deb064 (0064): .word (0) D/dalvikvm: 0x45deb068 (0068): .word (0x40d00000) D/dalvikvm: 0x45deb06c (006c): .word (0) D/dalvikvm: 0x45deb070 (0070): .word (0x3fe00000) D/dalvikvm: 0x45deb074 (0074): .word (0) D/dalvikvm: 0x45deb078 (0078): .word (0x3ff00000) D/dalvikvm: End LCalc;calcX, 6 Dalvik instructions. 

Самая интересная часть:

 vldr d8, [r15, #96] ; d8 := 1.0 vmov.f64 d9, d8 ; d9 := d8 vmov.f64 d10, d9 ; d10 := d9 // now d8, d9 and d10 contains 1.0 !!! vdivd d8, d8, d10 ; d8 := d8 / d10 = 1.0 vadd d8, d8, d9 ; d8 := d8 + d9 = 2.0 vmov.f64 d11, d8 

Ну, код, созданный JIT, выглядит совершенно неправильно. Вместо трех только одной константы читается 1.0, и в результате получаем вычисление X = 1.0 + 1.0 / 1.0 что не удивительно оценивает до 2.0

И вот вывод JIT для Dalvik запускается с kLoadStoreElimination оптимизацией kLoadStoreElimination (которая исправляет ошибку):

 D/dalvikvm: Dumping LIR insns D/dalvikvm: installed code is at 0x45d64000 D/dalvikvm: total size is 124 bytes D/dalvikvm: 0x45d64000 (0000): data 0x5260(21088) D/dalvikvm: 0x45d64002 (0002): data 0x4572(17778) D/dalvikvm: 0x45d64004 (0004): data 0x0044(68) D/dalvikvm: 0x45d64006 (0006): ldr r0, [r15pc, -#8] D/dalvikvm: 0x45d6400a (000a): ldr r1, [r0, #0] D/dalvikvm: 0x45d6400c (000c): adds r1, r1, #1 D/dalvikvm: 0x45d6400e (000e): str r1, [r0, #0] D/dalvikvm: -------- entry offset: 0x0000 D/dalvikvm: L0x45717274: D/dalvikvm: -------- dalvik offset: 0x0000 @ const-wide/high16 v0, (#16368), (#0) D/dalvikvm: 0x45d64010 (0010): vldr d8, [r15, #96] D/dalvikvm: -------- dalvik offset: 0x0002 @ const-wide/high16 v2, (#16352), (#0) D/dalvikvm: 0x45d64014 (0014): vldr d10, [r15, #76] D/dalvikvm: 0x45d64018 (0018): vldr d9, [r15, #80] D/dalvikvm: 0x45d6401c (001c): vstr d9, [r5, #8] D/dalvikvm: -------- dalvik offset: 0x0004 @ const-wide/high16 v4, (#16592), (#0) D/dalvikvm: 0x45d64020 (0020): vstr d10, [r5, #16] D/dalvikvm: -------- dalvik offset: 0x0006 @ div-double/2addr v0, v4, (#0) D/dalvikvm: 0x45d64024 (0024): vdivd d8, d8, d10 D/dalvikvm: -------- dalvik offset: 0x0007 @ add-double/2addr v0, v2, (#0) D/dalvikvm: 0x45d64028 (0028): vadd d8, d8, d9 D/dalvikvm: 0x45d6402c (002c): vstr d8, [r5, #0] D/dalvikvm: -------- dalvik offset: 0x0008 @ return-wide v0, (#0), (#0) D/dalvikvm: 0x45d64030 (0030): vmov.f64 d11, d8 D/dalvikvm: 0x45d64034 (0034): vstr d11, [r6, #16] D/dalvikvm: 0x45d64038 (0038): blx_1 0x45d63028 D/dalvikvm: 0x45d6403a (003a): blx_2 see above D/dalvikvm: 0x45d6403c (003c): b 0x45d64040 (L0x45718050) D/dalvikvm: 0x45d6403e (003e): undefined D/dalvikvm: L0x45718050: D/dalvikvm: -------- reconstruct dalvik PC : 0x457313f4 @ +0x0008 D/dalvikvm: 0x45d64040 (0040): ldr r0, [r15pc, #28] D/dalvikvm: Exception_Handling: D/dalvikvm: 0x45d64044 (0044): ldr r1, [r6, #108] D/dalvikvm: 0x45d64046 (0046): blx r1 D/dalvikvm: -------- end of chaining cells (0x0048) D/dalvikvm: 0x45d64060 (0060): .word (0x457313f4) D/dalvikvm: 0x45d64064 (0064): .word (0) D/dalvikvm: 0x45d64068 (0068): .word (0x40d00000) D/dalvikvm: 0x45d6406c (006c): .word (0) D/dalvikvm: 0x45d64070 (0070): .word (0x3fe00000) D/dalvikvm: 0x45d64074 (0074): .word (0) D/dalvikvm: 0x45d64078 (0078): .word (0x3ff00000) D/dalvikvm: End LCalc;calcX, 6 Dalvik instructions 

Выполняются все три константы, загруженные, как ожидалось, и правильная оценка.

Проблема, с которой вы столкнулись, может быть связана с аппаратным обеспечением процессора. В истории вычислений есть несколько известных примеров:
1994, Некоторые процессоры Intel Pentium имели ошибку, производя ошибки вычисления с плавающей запятой (ошибка FDIV). Это было только как 4-я цифра после десятичной точки. Intel в конечном итоге внедрила программу замены для замены дефектных процессоров на хорошие. DEC VAX 11/785 (введенный в 1984 году) имел конструктивный недостаток в его (необязательном) сопроцессоре с плавающей запятой. Из-за состояния гонки в аппаратном обеспечении иногда сопроцессор с плавающей запятой возвращал произвольное значение вместо желаемого результата на некоторых машинах. Корпорация Digital Equipment создала программу для замены сопроцессора (5 больших печатных плат) для всех клиентов с контрактом на техническое обслуживание оборудования.

Я бы предположил, что вы можете попробовать провести еще несколько тестов на более широкой аппаратной базе, чтобы лучше понять проблему. Если проблема будет действительно связана с оборудованием, я предполагаю, что ваш лучший подход – найти способ обойти проблему и документировать ее для других разработчиков.