пʼятницю, 29 березня 2013 р.

Помилка оптимізації GCC-AVR

Зображення взято тут: "The AVR and the Arduino"
Поширеною поміж програмістів, які працюють з контролерами, є думка, що користуватися засобами оптимізації компіляторів небажано -- може призводити до проблем. Не знаю, наскільки ця думка правильна в загальному, але проблеми справді трапляються. Зокрема, тому багато бібліотек для контролерів так і пишуть: "використовувати без оптимізації", або "за ввімкненої оптимізації можливі проблеми". Але й зворотній процес теж існує -- "ми знаємо, що від оптимізації проблеми, значить навіть не пробуватимемо"


Працюючи з ATMega2560 потрапив я в ситуацію, коли оптимізація критично необхідна -- контролером виконуються доволі важкі для нього обчислення, які без оптимізації сповільнюються в декілька раз. В процесі виявилося, що проблеми від неї справді бувають. Наприклад, запозичений із бібліотек Arduino бібліотека "Serial" недолюблює її, іноді загадково починаючи виводити сміття, а функції типу parseInt взагалі відмовляються працювати, навіть для O1.  Це було менш-більш очікуваним. Проте, в процесі роботи виявилося, що буває і гірше. Контролер, після кількох "робочих циклів", зависає. Після кількох годин безуспішних пошуків логічних помилок у коді підозра впала на витікання пам'яті (memory leaks).

Перш ніж переходити далі, кілька слів про робоче середовище:  використовую AtmelStudio, версії 6.0.1843 та avr-gcc 4.6.2, який йде в комплекті.  

Для перевірки цієї гіпотези було використано код, наведений тут:  "ATmega memory use".

int freeRam () {
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

Цей код знаходить віддаль між вершиною стеку (для її знаходження береться адреса нової автоматичної змінної, v) та вершиною купи (heap), котрі, як відомо, рухаються на зустріч одна одній. Вважається, що використовується gcc i avr-libc. (Див. рисунок за посиланням). Зрозуміло, що так неможливо коректно визначити вільну пам'ять -- реальна вільна пам'ять може бути більшою, якщо в купі є області, що вже були звільнені.

Але вже така проста перевірка показала, що в коді губиться пам'ять -- за кожного запуску величина, яку повертає freeRam, меншає. При тому, ділянка коду, де це стається, не містить ніяких динамічних виділень пам'яті, які можна б було забути звільнити. Своєрідним половинним діленням вдалося встановити, що витікання пам'яті відбувається в наступному коді:

float data[collection_size][19];

Жорстоке WTF?!!! Як?!

Дивлячись на контекст, автоматична змінна, розташована якось так:

void some_function()
{
...code...
  switch(some_val)
  {
...code...
    case SOME_CASE:
    {
        float data[collection_size][19];
        ...code...
    }
...code...
  }
...code...
}

Як тут може витікати пам'ять?! Чесно -- до сих пір не знаю. В асемблерний код не ліз, а по іншому не видно. Але експерименти показали, що проблема виникає лише коли включено оптимізацію O1 або більше.

Методом того ж "половинного ділення", розбиваючи групу опцій, що входять в  O1, виявив, що якщо заборонити оптимізацію -fsplit-wide-types (тобто, окрім -O1, дати команду -fno-split-wide-types), проблема пропадає, пам'ять перестає витікати. Не повертається вона і для -O3, якщо ця оптимізація вимкнена. Решта мого коду (не рахуючи Ардуїнівську Serial, яка і так -- не мій код :) чудово працювала з оптимізацією.

Чому та оптимізація давала  такий ефект, не знаю. Згідно офіційного мануала -- цілком невинна опція:
-fsplit-wide-types
When using a type that occupies multiple registers, such as long long on a 32-bit system, split the registers apart and allocate them independently. This normally generates better code for those types, but may make debugging more difficult.


Цікавий побічний ефект -- програма, із відключеною цією опцією, почала працювати на 2% швидше.

В процесі аналізу натрапив на посилання: "Avr-gcc Recommended Options", де стверджується, що опції:
  • -fno-inline-small-functions
  • -fno-move-loop-invariants
  • -fno-optimize-sibling-calls (впливає на avr-gcc починаючи з 4.7: PR34790)
  • -fno-split-wide-types
  • -fno-tree-loop-optimize
  • -fno-tree-switch-conversion (обхід помилки оптимізатора: PR49857).
  • -maccumulate-args (починаючи з  4.7: PR50887)
  • -mstrict-X (починаючи з4.7: PR46278)
можуть, забороняючи деякі оптимізації, допомогти покращити швидкодію коду, полегшуючи оптимізатору життя. Також там рекомендується декілька опцій, що обходять помилки оптимізатора:
  • -fno-dse (обхід PR50063 в 4.6)
  • -fno-caller-saves (обхід ряду внутрішніх помилок компілятора типу "spill failure", наприклад PR50925 та помилок оптимізатора типу PR52025).
  • -fno-tree-ter (обхід PR53033)
Детальніше див. за посиланням, але, як би там не було, знайдена мною проблема із  -fsplit-wide-types там не згадується. Нашвидкоруч проглянувши багтрекер gcc, знайшов лише один баг, що має відношення до AVR i цієї опції одночасно: Bug 35869. Але не схоже, що це воно... Гугл теж нічого не підказав. Якщо знайдеться час побудувати мінімальний приклад, треба буде багрепорт написати...

Як би там не було, цікава вийшла практична ілюстрація, що буває від оптимізацій для контролерів. Сподіваюся, це не спонукатиме відмовлятися від них! Швидше навпаки, шанс розібратися та ізолювати проблему завжди реальний.

Немає коментарів:

Дописати коментар