Pull to refresh

Comments 50

Что есть бинарная совместимость в C++? Как она реализуется?
Ну как бы… Совместимость с++ на уровне системных вызовов? Вроде бы нет. Совместимость на уровне процессора? Тоже мимо. Формат процедур? Какой угодно. Даже для одной ос/разрядности разные компиляторы из одного исходного кода сгенерят совершенно разный исполняемый.
В общем, если бы вы смогли объяснить суть проблемы, было бы неплохо.

Совместимость между разными компиляторами, между разными версиями одного и того же компилятора, или между разными версиями одного и того кода. Итоговая цель обычно в том, чтобы можно было независимо получать и обновлять бинарные артефакты. Например, чтобы обновляя библиотеку-зависимость в системе, быть уверенным в том, что приложение не сломается, потому что библиотека собрана чуть-чуть более новой версией компилятора, или в её исходном коде кто-то переименовал переменную.

То есть вся драма касается какой-то единственной реализации компилятора? Или компиляторов в одной ОС (и похоже это не win)? Как декорация имен может влиять на производительность? Как-то все еще не очень понятно.

Майкрософт и стандартам не особо следует, потому рассуждать о ABI в их случае — странно.
"Драмы" особой под тем же линуксом нет.
При чём тут производительность декорации имён — вообще не понятно.
Если у вас имена не совпадают — ничего работать не будет, но это таки нe ABI.

В статье жалуются на издержки производительности. Вы пишете про декорацию имен. Что из этого касается ABI — по прежнему непонятно.

В статье речь идет о том, что начиная с c++11 до c++20 код, собранный совместимыми компиляторами с разными версиями стандарта (из перечисленных) слинкуется.
Проблемы производительности в том, что стандарт предписывает реализацию {X} для класса T, тогда как реализация {Y}, ни в каком виде не совместимая бинарно (компилятор сгенерирует другой код и другую структуру для хранения в памяти), но полностью совместимая по API (ходит и крякает так же), может быть намного эффективнее.

В любом случае фрагментация по бинарной сомвместмости высока и все равно большинство библиотек проще (лучше?) самим пересобрать, чем искать собранную неизвестно кем с неизвестно какими параметрами. Т.е. рядовому пользователю пересобрать проект ради +10% производительности это просто божий дар.


Гораздо больше беспокоит историческое наследие в апи и что доступ к стандартам надо покупать (дикость для 2019го). Имхо лучше бы сломали к черту совместимость объявив c++ next, несовместимый с предыдущим, как сделали в Питоне. Что касается стоимости переписывания, так надо и стоимость дальнейшей поддержки учитывать. Сейчас и предыдущие стандарты не полностью поддерживаются компиляторами. И толку заботится об api и abi, если даже крупные корпорации не в состоянии за 5+ лет внедрить новшества? Подозреваю что в первую очередь из-за переусложненности языка и исторического наследства. Фактически у нас и сейчас несколько разных диалектов c++ современных стандартов, некоторые до сих пор 98 стандарт используют, а кто-то и вовсе переходит на раст, го, так что терять нечего, комитет, жги.

доступ к стандартам надо покупать (дикость для 2019го)

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


Можно и не участвовать в крысиных бегах, смотреть на открытые черновики стандарта, и делать как считаете нужными. У вас, правда, не будет плашечки Оффициально™ Соответствует Стандарту ISO, которая что-то значит для людей, не разбирающихся в реализации C++.

Имхо лучше бы сломали к черту совместимость объявив c++ next, несовместимый с предыдущим, как сделали в Питоне.
вот как раз и не хотят «как в питоне». Для сравнения: сейчас средний с++ проект уже с modern c++, а питон2, поддержку которого уже собираются прекращать, живее всех живых.
Проблемы производительности в том, что стандарт предписывает реализацию {X} для класса T, тогда как реализация {Y}, ни в каком виде не совместимая бинарно (компилятор сгенерирует другой код и другую структуру для хранения в памяти
Один комментатор ниже утверждал, что «проблемы общеизвестны людям, которые профессионально пишут на C++». А оказывается, каждый видит свои — кто в декорировании имён, кто в layout-ах структур в памяти (а как последние влияют на эффективность передачи unique_ptr, о которой пишет автор?).

Поэтому и надо сначала обозначить проблему чётко и ясно, а потом уже обсуждать.

Декорация имен является частью ABI, так как определяет какими именно будут имена сущностей C++ на уровне объектных файлов и DSO.


Если имена получаются не удобными для машины, то увеличиваются затраты на обработку объектных файлов и линковку с библиотеками. Грубо говоря, одна из "проблем" в том, что имена получаются длинные (килобайты из шаблонов) и библиотеки просто пухнут (нередко 10-20% объема файла, после обрезки debug info).


В случае с DSO (aka dll на Windows) добавляются затраты на связывание в runtime. Может показаться неожиданным, но загрузка и запуск большого проекта C++ клубком DSO может приводить к таблицам имен в 10-100 тысяч имен по 20-1000 байт длиной каждой. И всё это потому что про декорацию особенно не думали, тем более "не завезли" visibility, а потом не решились на visibility=hidden по умолчанию. Короче, печаль.

не решились на visibility=hidden по умолчанию. Короче, печаль.
В чём проблема-то? Клиент библиотеки включает в свои таблицы импорта только те символы (методы, переменные), к которым фактически имеет доступ, а не все 100500 известных.
Сама библиотека может экспортировать только те символы, которые автор библиотеки посчитает нужным видеть клиентам (в MSVC это настраивается в def-файле). Вы бы хотели на уровне языка отмечать у членов класса видимость, чтобы было стандартизовано и не требовалась возня с implementation-specific фичами? Так пожалуйста, вводите в стандарт, от того, что библиотека экспортнёт не все символы, а только фактически используемые, ABI не ломается.
Статьи, выдаваемые гуглом, утверждают, что ABI это фактическая реализация конструкций языка, к примеру rtti, виртуальных функций, ну и тех же декораций. В этом случае ABI у M$, естественно, есть, просто свой (как и у остальных, впрочем). Но при чем тут производительность, или стандарты языка?
UFO just landed and posted this here

Интерфейс бинарника стараются сохранить.
Например декорация имён, порядок передачи аргументов в функции и т.п.
Это позволяет абстрагироваться от "формата процедур" и "системных вызовов" и относительно просто организовать взаимодействие используя заголовочные файлы и здравый смысльпри проектировании библиотек.
Естественно, что если полезть в потроха бинарника, который собран другой версией компилятора — никто никаких гарантий вам не даст.

UFO just landed and posted this here

Это и есть несовместимость на уровне ABI. Но только это не проблемы стандарта C++, а результат сознательной преднамеренной (деструктивной) политики M$ (что неоднократно признавали топовые разработчики покинувшие анклав M$, и что легко проследить начиная от \r\n до OID-ов в ActiveDirectory), в том числе и в отношении ABI.

UFO just landed and posted this here
Плохая статья. Она говорит о какой-то проблеме, но не объясняет проблему. Достаточно было бы дать ссылку, если автор считает, что вопрос общеизвестен и не требует детализации.

Oн(вопрос) и она(проблема) общеизвестны людям, которые профессионально пишут на C++.
А хейтерам и ссылки не запретят возмущаться.

UFO just landed and posted this here
Они то может и представляют, но зачем сюда постить перевод, не вводя читателей в проблему. У меня может на работе тоже есть интересные задачи, но вырывать кусок из середины рабочей переписки и постить — не оценят.
Проблема в необходимости прописывать
_GLIBCXX_USE_CXX11_ABI=0
и пересобирать все используемые библиотеки (и сам проект) для того, чтобы подключить какую-то старую библиотеку, которую пересобрать из исходиников тяжелее (или невозможно)
Вот оно что… Я уж было подумал, что что-то глобально хотят переделать в кодогенерации и соглашениях о вызовах. А тут всего-лишь расширение классов типа std::string, которое естественно потребует перекомпиляции всех модулей, шарящих между собой экземпляры таких классов.

Тоже, конечно, важно. Но я бы предложил везде, где нужна производительность, линковать всё в один большой толстый бинарник, как это делает например Go (память сейчас не проблема). Заодно Whole Program Optimization и агрессивные inline-ы добавят к скорости.

Либо, если хочется маленьких бинарников и динамической линковки (для утилит вроде cat и cp), не использовать на критических участках std::string, std::list, сделать std-классы максимально совместимыми (пусть и медленными при вызовах между границами модулей).
Насколько я понимаю, бинарная совместимость только в рамках одного компилятора, потому что даже соглашения о вызовах в разных компилятора разная. Иногда от версии к версии оно может меняться, правда редко очень. Для это, например, мы храним с проектом сразу и версию компилятора, на которой он был собран, чтобы потом в случае чего получить тоже самое на выходе.
Хочется привести хоть один реальный пример, когда комитет отказался менять API на потенциально более производительное ради сохранения ABI.
std::set<std::string> s = ...;
s.find("very very long string"); // конструирование std::string здесь

Если бы после появления std::less<void> в C++14 определение std::set поменяли с
template<class Key, class Compare = less<Key>, ...>
class set {...};
на
template<class Key, class Compare = less<void>, ...>
class set {...};
предыдущий пример не требовал бы создание временной std::string, но это сломало бы ABI.

Вы путаете (как минимум в примерах) совместимость на уровне API (компилируется или нет) и ABI (работает ли скомпилированное).


Совместимость на уровне API стараются сохранить, но смело ломают когда есть резон. А совместимость на уровне ABI со стороны WG21 менялась (если не ошибаюсь, но другого не помню) только с приходом COW для std::string в C++11. При этом было постелено немало "соломки", поэтому GNU libstdc++ и сейчас обеспечивает работу софта собранного для старого ABI.


При этом WG21 примерно не причастен к массе граблей (например) в разных версиях MSVC.

В каком случае в моем примере был бы сломан API? Весь код, компилирующийся в предположении, что set<T> это set<T, less<T>>, продолжит компилироваться если set<T> станет set<T, less<void>>.

Возможно я что-то неверно понял/воспринял в вашем комментарии, но показанное изменение определения std::set<> это смена API и ABI одновременно, причем смена API нивелируется/маскируется гибкостью C++.


Поэтому вместо смены ABI в С++14 расширили API добавив template< class K > iterator find( const K& x ) во все релевантные контейнеры из Containers library.

И этот самый template<class K> iterator find(const K& x) не помогает избежать создания временной строки при вызове s.find("very very long string") из моего первого примера так как less<string> все равно ожидает 2 аргумента типа string.

Опять-таки не уверен, что я вас правильно понял, но что мешает задействовать свой template<typename T = void> struct less c несколькими перегруженными operator()()?

Можно задействовать и свой компаратор и стандартный std::less<void>, но это надо делать явно, я же говорил о том, что комитет решил не делать это поведением по умолчанию std::{set,map} чтобы не ломать ABI.

Линукс: несколько лет назад уже было изменение ABI в такой небольшой штуке как std::string. Затронуло оно чуть менее чем весь софт на C++.


Винда: совместимость между кодом, собранным разными версиями студии есть только в рамках VS2015-2019.


О каких "более 10 лет бинарной совместимости на многих платформах говорит автор"?

Во-первых dual ABI в GNU libstdc++ как-раз таки обеспечивает совместимость на уровне ABI. Т.е возможно не только запустить старый софт на новой системе, но даже и на новой системе собрать софт, который будет работать на старой (может потребоваться некоторое шаманство для отвязки от symver для некоторых функций glibc).


Во-вторых WG21 не причастен к граблям поставленных M$ с приходом "Universal Runtime" в MSVC (M$ делает подобное регулярно).

Во-первых dual ABI в GNU libstdc++ как-раз таки обеспечивает совместимость на уровне ABI.

ABI базового C++-класса изменилось. Кто в этом виноват, какие костыли можно применить чтобы оттянуть во времени неизбежное (переход на новый ABI) — не важно. По факту пользователи C++ страдали, наступали на проблемы, пересобирали кучу софта, обмазывали подпорками то что пересобрать нельзя и т.д.

Способов страдать бесконечно много. Наверное, умеючи, можно и Dual ABI задействовать с этой целью ;)


Мне пришлось где-то добавить -D_GLIBCXX_USE_CXX11_ABI=0 в CXX_FLAGS, а пару раз даже для удобства приписать "#define _GLIBCXX_USE_CXX11_ABI 0" в системные h-файлы. Но на страдания это никак не тянет.


Хотя если принципиально избегать RTFM, то можно позабавится ;)

приписать "#define _GLIBCXX_USE_CXX11_ABI 0" в системные h-файлы

Прелестно.


Хотя если принципиально избегать RTFM

  1. Ваш намёк не вполне понятен.


  2. У Буратино есть два .so, один собран со старым ABI, второй с новым. Расскажите, в какое положение ему надо переключить _GLIBCXX_USE_CXX11_ABI при сборке .cpp-файла, использующего код из обоих .so.


приписать "#define _GLIBCXX_USE_CXX11_ABI 0" в системные h-файлы
Прелестно.

На всякий поясню — добавлялось в сборочные контейнеры для сборки "старых" проектов, чтобы совсем не трогать исходники с определениями опций сборки (включая CXX_FLAGS). В частности, так собирались so-шки со "старыми" строками с использованием новых toolchains и без модификации исходников (иногда принципиально важно).


Хотя если принципиально избегать RTFM
Ваш намёк не вполне понятен.

Ради бога не подумайте что я хотел вас задеть и т.п. Просто большинство страданий обычно от незнания.


У Буратино есть два .so, один собран со старым ABI, второй с новым. Расскажите, в какое положение ему надо переключить _GLIBCXX_USE_CXX11_ABI при сборке .cpp-файла, использующего код из обоих .so.

Ну вы же сами прекрасно понимаете, что Буратине так не помочь и ему придется выбрать один из берегов (и какую из so-шек пересобрать). Но только это не страданья, а какая-то недодуманность при сборке so-шек и/или выборе поставщиков.


Т.е. достаточно очевидно, что Буратине нужно определиться с политикой (выбрать одно из трех: только старый стандарт, только новый, либо сборка двух версий so-шек) и просто следовать выбору. Если уже только это приносит Буратине страданья, то его дела плохи — вскорости любые изменения начнут приносить ему боль (включая смены дня и ночи).

Винда: совместимость между кодом, собранным разными версиями студии есть только в рамках VS2015-2019.
О каких «более 10 лет бинарной совместимости на многих платформах говорит автор»?
MS не особо пытаются сохранить бинарную совместимость между разными версиями MSVS. Была, например, поломка ABI в 15.6 — минорной! версии. Лучше смотрите в сторону libstdc++, например.

Меня печалит, что автор (Тит Винтерс) рассуждает и как-бы направляет WG21 в неверном направлении выбора между прогрессом и совместимостью. ABI не может быть вечным, его всё-равно неизбежно когда-нибудь придётся развивать/менять. Выражаясь вульгарно — Не следует обсуждать когда и как лучше кошке отрезать хвост: сегодня, завтра, сразу или частями. Нужно думать про анестезию.


IMHO поэтому WG21 следует сосредоточится (хотя не уверен что это именно их задача) на выработке соглашений/рекомендаций/рецептов для совместного использования нескольких ABI, в том числе для предотвращения смешивания вызовов разных реализаций (один DSO создает новый std::string, а другой DSO разрушает его деструктором от старого std::string).


Это бы не только сняло ряд проблем с развитием C++, но и буквально развязало руки реализаторам (просвистел помидор в сторону компилятора C++ для E2K). А если помечтать, то попутно можно было-бы дожать проблему с "километровыми" идентификаторами в DSO и т.д. и т.п. Но мне кажется, что большинство "коммитетных парней" чураются спускаться до уровня ABI и DSO, предпочитая рассуждать от высоких материях. Поэтому будем ждать когда условные Google или Apple снова занесут гранты реализаторам в обход "коммитетов".

В совместимости уже нет такой необходимости, как раньше. За то время что я в IT было создано более 10 платформ практически с нуля. В мире много программистов и они что надо напишут/перепишут. А скорость сейчас всё важнее, прогресса большого у процессоров уже не наблюдается, надо выжимать что есть.
грубо говоря, можно сначала ввести механизм (пример) по смешиванию разных версий языка, добавить в него методы конвертации между ABI*, а это уже и есть возможность менять ABI сохраняя обратную совместимость.

* скажем, если при вызове foo(std::unique_ptr) из более новой «версии» в старую нужно лишь использовать другое соглашение о вызовах, то вызов foo(const std::string&) может создать проблемы, захоти мы поменять бинарное представление std::string. Для этого потребуется введение методов конвертации классов между версиями стдлибы. По сути, механизм для этого почти существует (inline неймспейсы/алиасы и пр.), не хватает лишь указания версии для парсинга.

Плохо понял, о чем речь? Совместимости ABI в C++ никакой нет, библиотека, скомпилированная одним компилятором, не линкуется с другой. У каждого компилятора своя реализация "стандартной библиотеки".


Что касается совместимости в пределах одного компилятора, то в чем проблема сделать просто libcpp1 и libcpp2 и пусть старые проекты используют libcpp1, а новые — libcpp2? Для надежности можно поменять способ name mangling, чтобы при использовании неправильной библиотеки все рушилось с грохотом.


Или же сделать 2 версии функций с суффиксами: function и function2.


Что-то у меня ощущение, что проблемы будут разве что в скомпилированных 20 лет назад на проприетарном компиляторе библиотеках, от которых нет исходников.

Не то чтобы вы совсем не правы, но неправы достаточно значительно:


  • Есть проблемы порожденные отсебятиной M$ (off-topic: Microsoft регулярно кидает и подставляет разработчиков, но некоторые всё еще терпят. Последний пример = "TxF may not be available in future versions of Microsoft Window"). Эти проблемы видны только в некоторых кросс-платформенных сценариях и все уже примерно привыкли что "на виндовс всё через Ж".
  • В 90% остальных случаях есть GCC и CLANG, где с совместимостью ABI очень хорошо, если не прекрасно.
  • Есть embedded-like платформы, где местами есть некий зоопарк ABI и проприетарных компиляторов, хотя наблюдается устойчивая тенденция к принятию "ABI как в GCC" в качестве стандарта де-факта. Но в основном тут разные ABI по-определению (для разного порядка байт, PIC/неPIC и т.п.).
  • Есть какие-то старые платформы (Alpha, DOS) для которых нет актуальных ABI и если вы найдете или соберете компилятор, то ему как-бы не с чем быть несовместимым по ABI.

Поэтому в 99.99% случаев вы получите совместимость по ABI от разных компиляторов, либо вам просто нужно знать что вы хотите и затребовать это задав опции.

Вы описали столько различий, Microsoft не пользуйте, старое не трогайте, ембеддед — зоопарк, а потом написали, 99.99% получите совместимость. Вот у меня даже от версии к версии мой IAR по ABI несовместим, как вы и говорите, на каждой версии устойчивая тенденция к изменению все ближе и ближе к GCC, но от этого не легче.
с++ позволяет сохранять бинарную совместимость за парой исключений типа запрета на COW для стандартных контейнеров и багфикса на «noexcept — часть сигнатуры функции» (который, кстати, и нужен для сохранения бинарной совместимости в будущем).

Всё остальное в руках поставщиков компиляторов. Если майки не ставят перед собой цель сохранять бинарную совместимость, её и не будет — им нравится вариант «просто тащим все версии всего» (например, у меня сейчас установлено 9 версий c++redist).
Sign up to leave a comment.

Articles