пʼятниця, 13 листопада 2015 р.

Мікро-реалізація стандартної бібліотеки C++ -- uClibc++

Саморобна амфібія. (c) wiki
Якщо libcxxrt підміняє собою libsupc++, то uClibc++ намагається бути всім решта із libstdc++, що не входить в libsupc++. Тобто, для свого функціонування вона потребує libsupc++ (або libcxxrt) -- бібліотеку низькорівневої підтримки.

Подивимося, що це за звір. Скачати її можна на офіційному сайті. В архіві є файл INSTALL, в якому описана процедура інсталяції. На жаль, вона доволі нетривіальна і сильно зав'язана на Linux (хоча, перенесення під Windows видається можливим). Перший етап -- "make menuconfig", генерує конфігурацію бібліотеки -- можливості, які слід включити. Результатом його буде файл system_configuration.h, із купою макросів виду "#define __UCLIBCXX_STL_BUFFER_SIZE__ 32". Після чого слід виконати хитрі магічні процедури, в результаті яких буде отримано незалежний lib-файл.

Для економії часу, я схитрував --- критично важливий system_configuration.h згенерував на доступній мені Linux-машині, а решту операцій уник, додавши файли бібліотеки до проекту. Для ясності, це може виглядати так -- в директорії проекту створюється директорія uclibcxx, до неї копіюється вміст директорій src та include із архіву текстів бібліотеки, крім Makefile-ів та піддиректорії src/abi -- вона буде зайвою. В include кладемо і system_configuration.h. В середовищі створюємо відповідні групи, додаємо до них файли із цих директорій. (Готовий демонстраційний проект CoIDE, як завжди, можна скачати тут).

Додаємо libsupc++ до списку бібліотек (як це робити, не раз згадувалося в попередніх постах) -- uClibc++ покладається на неї. (libstdc++, навпаки, повністю самодостатня, фактично є "два-в-одному"). Також, вважаємо, що С-ний runtime, про який говорили раніше, присутній. "Свій" С++-ний runtime не додаємо --- цим займається libsupc++.

Пробуємо компілювати. Халяви немає -- купа помилок.



1. В файлі unwind-cxx.h є ось така конструкція:

// This is the exception class we report -- "GNUCC++\0".
const _Unwind_Exception_Class __gxx_exception_class

= ((((((((_Unwind_Exception_Class) 'G' 
  << 8 | (_Unwind_Exception_Class) 'N')
 << 8 | (_Unwind_Exception_Class) 'U')
       << 8 | (_Unwind_Exception_Class) 'C')
      << 8 | (_Unwind_Exception_Class) 'C')
     << 8 | (_Unwind_Exception_Class) '+')
    << 8 | (_Unwind_Exception_Class) '+')
   << 8 | (_Unwind_Exception_Class) '\0');

Компілюватися вона відмовляється. В принципі, це не дуже дивно --- наш компілятор версії 4.8, автор користувався 3.3.3, (десь траплялася згадка про трохи свіжіший, 4.3, не знайшовши її повторно, можу помилятися) -- може колись воно і компілювалося. Замінив на тривіальне:

const _Unwind_Exception_Class __gxx_exception_class="GNUCC++";

Зараз _Unwind_Exception_Class  оголошено як:

typedef char _Unwind_Exception_Class[8];

Можливо, колись це був якийсь довгий, 64-бітний, int? В файлі include/support цей тип оголошено по іншому -- якраз цілим, однак компілятор підхоплює його з іншого, системного, файлу unwind-arm-common.h. Не став розбиратися глибше.

Взагалі, проблема із використанням не тих файлів заголовків доволі серйозна, тому краще включати їх як `#include "..."`. Можна замість цього, для гарантії, передати компілятору опцію "-nostdinc++". (В демонстраційному проекті так і зроблено.)

2. Що мене дуже здивувало, файл src/support.cpp не може компілюватися взагалі, так як пробує скористатися вкладеними /* */ коментарями. Так як він мав би бути порожнім, підозрюю, штатна система компіляції його просто не компілює. Виправив, написавши #if 0 .... #endif, хоча, можна було повністю викинути.

Тепер бібліотека успішно компілюється. Пробуємо використовувати (користуючись тією ж "жертвою", що розглядалася і попередніх постах.

Знову маємо ряд проблем:

А. Про С++11 мова не йде, бібліотека написана до появи цього стандарту, і радикально не переписувалася останні роки -- використання її елементів слід вимкнути.

Б. Оператор == для set не компілюється. Виправляється викиданням цього оголошення із файлу include/set:

/*
 template <class Key, class Compare, class Allocator> _UCXXEXPORT bool operator==
  (const multiset<Key,Compare,Allocator>& x, const multiset<Key,Compare,Allocator>& y)
 {
  if(x.data == y.data){
   return true;
  }
  return false;
 }
*/

Ретельно не перевіряв, але після цієї зміни, порівняння, так виглядає, працюють коректно. Інші асоціативні контейнери таких проблем не мають.

В. Оператор == для queue (не плутати із deque!) теж не компілюється, скаржачись, що поле c -- захищене. Воно справді захищене. Виправив, зробивши цю функцію другом класу (і, для простоти, помістивши визначення прямо в оголошення класу). Зрозуміло, що і з іншими операторами, які стосуються queue, operator< , operator!=, тощо, буде та ж сама проблема. (Але поки, для економії часу, не виправляв. Можливо, займуся окремо -- це корисно, з точки зору вивчення стандартної бібліотеки С++).

Г. Для multimap operator==  оголошено, але не визначено, при тому, це оголошення конфліктує із аналогічним із __base_associative. (Закоментувавши, можна скомпілювати, працездатність не тестував). Скільки там ще таких косяків...

Ґ. Це не зовсім проблема, швидше --- дозволена стандартом ситуація, але наголосити варто. Ось такий код не компілюється:

printf("Reverse iterator base--: %i\n", *(--ritr.base()));

через те, що ітератор є вбудованим типом (звичайним вказівником), а до такого результату функції не можна застосовувати "--", він не є lvalue. Проблема відома, описана в книзі  "Эффективное использование STL" Скотта Мейерса, совет 28, "Научитесь использовать функцию base", стор. 119. Обходимо її описаним там способом:

printf("Reverse iterator base--: %i\n", *(++ritr).base());

Д. Сама бібліотека чесно (попередженням) зізнається, що файл limits -- неакуратний.

Е. Аналогічно, сама зізнається, що для map використовує deque, щоб економити пам'ять (детальніше мотивацію див. у офіційному FAQ, в такому рішенні може бути сенс).

Є. Там же зізнається, що підтримки локалей свідомо немає -- це надміру громіздка річ.

Ж. В купі файлів є такі коментарі, стосовно, зокрема, тих же операторів порівняння:
/* Non-member functions.  These are at the end because they are not associated with any
   particular class.  These will be implemented as I figure out exactly what all of
   them are supposed to do, and I have time.

 */

Питання розміру 


Програма, яка використовує ряд можливостей STL (текст див. в архіві з проектом), скомпільована із використанням libstdc++ та uClibc++. Після згаданих вище правок (повторюся, вони далеко не вичерпні!), поводяться обидві програми однаково, а їх розмір:


libstdc++uClibc++

text data bss Разом text data bss Разом
O0 122924 2256 2208 127388 108920 2272 400 111592
Os 81088 2256 2208 85552 85580 2272 400 88252
Os, LTO 80192 2236 2208 84636 83636 2288 400 86324








-Ofast 82200 2256 2208 86664 86792 2272 400 89464
-Ofast,LTO 81244 2236 2208 85688 87724 2288 400 90412









-O3 82200 2256 2208 86664 86792 2272 400 89464
-O3, LTO 81244 2236 2208 85688 87724 2288 400 90412

Виграш в розмірі - ну, скажу делікатно, "не туди". Прошивка, крім варіанту без оптимізації -- тільки зросла. Однак, є дуже значимий виграш -- в розмірі потрібної оперативної пам'яті -- на 1800 байт (майже четвертина від наявної). Іноді це дуже важливо. (Хоча, ймовірно, libstdc++ використовує якісь статичні буфери, які теж можна зменшити). Ще один важливий аспект -- бібліотека дозволяє вибірково викидати компоненти -- див. system_configuration.h нижче.

Цікаві побічні спостереження --- для даної програми -Ofast не додав нічого нового, LTO зменшує розмір прошивки всього на відсоток з хвостиком, і, явно, "вбиває" якийсь продубльований об'єкт (константу?), зменшуючи потрібну RAM на 20 байт для libstdc++, однак, збільшує на 16 для  uClibc++ -- як?!

Потоки та стрічки

Не зважаючи на вищесказану критику, з використанням libstdc++ ви на цьому контролері аж ніяк не зможете зробити таке:

        cout << "Test:" << right << setw(20) << 555 << endl;

        stringstream ss;
        ss << "Test:" << right << setw(20) << 555 << endl;
        cout << ss.str() << flush;

А з uClibc++ воно чудово працює. (Увага -- тут важливо, щоб випадково не підхопило системний iostream -- ймовірно, все компілюватиметься, але виводу не буде). При чому, якщо додати ці рядки до програми, що тестувалася вище, то:


cout
cout + stringstream 

text data bss Разом text data bss Разом
Os, LTO 84544 2288 400 87232 86656 2288 400 89344









Тобто, використання cout з парою маніпуляторів додало менше кілобайта до пам'яті програм 16 байт до використання RAM, а stringstream -- ще трішки більше кілобайта, без зайвих витрат RAM. Непоганий результат.

Демонстраційний проект для CoIDE, тут.

system_configuration.h

Наведу вміст цього файлу -- з назва макросів видно, які можливості присутні, та що із того можна викинути для економії.

/*
 * Automatically generated C config: don't edit
 */
/*
 * Version Number
 */
#define __UCLIBCXX_MAJOR__ 0
#define __UCLIBCXX_MINOR__ 2
#define __UCLIBCXX_SUBLEVEL__ 4

/*
 * Target Features and Options
 */
#define __UCLIBCXX_HAS_FLOATS__ 1
#define __UCLIBCXX_HAS_LONG_DOUBLE__ 1
#define __UCLIBCXX_HAS_TLS__ 1
#define __WARNINGS__ "-Wall"
#define __BUILD_EXTRA_LIBRARIES__ ""
#define __HAVE_DOT_CONFIG__ 1

/*
 * String and I/O Stream Support
 */
#undef __UCLIBCXX_HAS_WCHAR__
#define __UCLIBCXX_IOSTREAM_BUFSIZE__ 32
#define __UCLIBCXX_HAS_LFS__ 1
#define __UCLIBCXX_SUPPORT_CDIR__ 1
#define __UCLIBCXX_SUPPORT_CIN__ 1
#define __UCLIBCXX_SUPPORT_COUT__ 1
#define __UCLIBCXX_SUPPORT_CERR__ 1
#undef __UCLIBCXX_SUPPORT_CLOG__

/*
 * STL and Code Expansion
 */
#define __UCLIBCXX_STL_BUFFER_SIZE__ 32
#define __UCLIBCXX_CODE_EXPANSION__ 1
#define __UCLIBCXX_EXPAND_CONSTRUCTORS_DESTRUCTORS__ 1
#define __UCLIBCXX_EXPAND_STRING_CHAR__ 1
#define __UCLIBCXX_EXPAND_VECTOR_BASIC__ 1
#define __UCLIBCXX_EXPAND_IOS_CHAR__ 1
#define __UCLIBCXX_EXPAND_STREAMBUF_CHAR__ 1
#define __UCLIBCXX_EXPAND_ISTREAM_CHAR__ 1
#define __UCLIBCXX_EXPAND_OSTREAM_CHAR__ 1
#define __UCLIBCXX_EXPAND_FSTREAM_CHAR__ 1
#define __UCLIBCXX_EXPAND_SSTREAM_CHAR__ 1

/*
 * Library Installation Options
 */
#define __UCLIBCXX_RUNTIME_PREFIX__ "/usr/uClibc++"
#define __UCLIBCXX_RUNTIME_INCLUDE_SUBDIR__ "/include"
#define __UCLIBCXX_RUNTIME_LIB_SUBDIR__ "/lib"
#define __UCLIBCXX_RUNTIME_BIN_SUBDIR__ "/bin"
#define __UCLIBCXX_EXCEPTION_SUPPORT__ 1
#define __IMPORT_LIBSUP__ 1
#define __IMPORT_LIBGCC_EH__ 1
#define __BUILD_STATIC_LIB__ 1
#undef __BUILD_ONLY_STATIC_LIB__
#undef __DODEBUG__


Висновки

Бібліотека доволі неоднозначна, хоч мені вона подобається. Плюси:
  • Компактність джерельних текстів -- вона значно менша, ніж "реалізація з коробки".
  • Відносна простота і зрозумілість (якщо не вірите -- загляньте у файли заголовків GCC та порівняйте.
  • Внаслідок компактності, можливість користуватися на контролерах потоки та стрічки.
  Мінусів, на жаль, теж немало:
  • Складна система компіляції. В принципі, вирішується. :-)
  • Стійке відчуття певної неохайності -- див. описані вище проблеми. Через що невідомо, наскільки їй можна довіряти без тестування кожного шматочка.
Можливо, колись спробую прикластися до доведення її до ладу.  А поки, чи користуватися -- вирішуйте самі. Я б в production, без дуже ретельного тестування, не ризикнув. Але штукенція, сама по собі, скажімо -- як навчальний проект, цікава.

Скоро ми до контролерів ще повернемося, а на разі:

Дякую за увагу!


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

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