Подтвердить что ты не робот

Ошибки вычисления значения двойной точности на процессорах 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" в манифест приложения) также исправляет ошибку.

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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

Ответ 3

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

Они появляются иногда в нескольких списках рассылки 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. Наслаждайтесь их настройками, надзорами и ошибками реализации. По крайней мере, это не повреждение вашей виртуальной машины.

Ответ 4

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

  • Эта ошибка была замечена ранее пользователями устройств MT6589 (например, здесь и здесь)
  • Общим решением было отключить JIT (для конкретного приложения или для всей системы).
  • Я смог воспроизвести эту проблему на двух устройствах с MT6589 и MT8125/8389, , в настоящее время он не был воспроизведен на устройствах с чипами, отличными от упомянутых, см. разделы ниже
  • Выражение, которое воспроизводит ошибку, если намного проще, чем я опубликовал в вопросе, просто:
    X = A + b / D
  • Задержка между вычислениями является решающей частью ошибки: без нее ошибка появляется спорадически, с небольшим сном после вычисления она воспроизводит всегда (как только код был JITed)
  • Я создал script, который собирает простой файл 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 (который исправляет ошибку):

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

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