Как стать автором
Обновить
18
0
Евгений Неручек @reficul0

Разработка по ту сторону кроличьей норы

Отправить сообщение

Кажется, что нам не всегда нужна синхронизация. Синтетический пример:

int func() {
  constexpr int min_offset = std::hardware_destructive_interference_size / sizeof(int);
  
  std::vector<int> res( min_offset + 1, 0 );
  std::thread t1( task1, &res[0] );
  std::thread t2( task2, &res[min_offset] );
  t1.join();
  t2.join();
  return res[0] + res[min_offset];
}

Да, я и правда не уточнил, что речь шла про вставку в конец. Делать вставку не в конец - очевидно гиблое дело. Исправил, спасибо.

Если говорить коротко, то, да, рано или поздно управляющий поток перейдёт границу, чтобы обратиться к имплементации STL из libc++ (clang), libstdc++(gcc), либо же имплементации от msvc.

Вопрос ABI совместимости в STL был очень хорошо разобран в статье Binary Banshees and Digital Demons с перпективы автора многих proposal-ов в стандарт. К сожалению, я не видел версии этой статьи на русском, лично мне она кажется стоящей перевода.

Вольно переводя материал этой статьи и немного утрируя, можно сказать следующее:

Проблемы с менеджментом памяти и конструированием/деструкцией контейнеров можно решить при помощи std::polymorphic_allocator, с его std::memory_resource интерфейсом, который гарантирует ABI совместимость между разными версиями STL. Это значит, что если в вашей программе вы используете header из новой версии STL с более старым бинарём самой стандартной бибилиотеки (libc++ / libstdc++), то у вас гарантированно должно всё работать.

Но решение с std::memory_resource проблемно, и суть проблема та же, что и у Си API. Дизайн решения - устаревший, и лучше уже не станет:

  1. Дизайн std::memory_resource был зарелижен уже устаревшим, вдохновлённым std::moneypunct (дизайном локали из C++98-эры!).

  2. Лучше этот интерфейс уже точно не станет, потому что он связан с двух сторон: с одной стороны - требование полной ABI-совместимости, а с другой - его реализация на виртуальных функциях.

    В резульате, улучшения стандартного аллокатора в новых версиях стандарта просто неприменимы для std::polymorphic_allocator. Когда в std::allocator добавляют allocate_at_least, то же самое уже не добавят в std::polymorphic_allocator, потому добавление новой фунции сломает ABI совместимость.

Здравствуйте! Наверное, вы имели ввиду, что нужно использовать Си API.

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

На самом деле есть ещё более простой совет - лучше не приводить к нарушению бинарной совместимости. Проще использовать header-only версию библиотеки, либо же пересобрать её под целевой тулсет, либо же найти уже пересобранную.

Я писал про это в выводе: "В одном чёрном-чёрном доме ..".

Из вывода

Насколько вы поняли, реальность ABI совместимости в C++ довольно сурова. Ещё более суровым её делает тот факт, что только при использовании Си API решение становится по-настоящему "бинарно-дружелюбным". Но, в то же время, со всем моим уважением к Си, код потеряет ту маленькую долю лакончиности и удобства, которое даёт нам С++, хотя, кажется, в тех редких случаях когда нам нужна 100% совместимость, другого выбора у нас нет.

Вы всегда можете написать Си API для своего юзкейса, перейти на использование POD типов, но на самом деле наилучший совет для перехода бинарной границы модулей с разным ABI — не переходить её. Чаще всего игра не стоит свеч (и в очередной раз отстреленных ног), поэтому проще использовать header-only версию библиотеки, если она есть. Или же пересобрать библиотеку под свой тулсет, или найти уже пересобранную.

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

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

Почему ваши замечания неконструктивны?

Основываясь на выбрке ваших высказываний (см. внизу), многие ваши замечания содержат вашу субьективную оценку. В связи с чем, ваши замечания - не конструктивны.

  1. https://habr.com/ru/articles/710658/#comment_26055918

    Простите, а когда вы черпаете вот эти свои знания из советских газет 

  1. https://habr.com/ru/articles/710658/#comment_26055960

    Простите, но это всего лишь ваше утверждение, ни на чем не основанное. Ну кроме фантазий со Stack Overflow.

  2. https://habr.com/ru/articles/710658/#comment_26078678

    Зачем вот эти кривляния уже не в первый раз

Почему ваши замечания содержат логические ошибки?

Вы допускаете логические ошибки следуя ненаучной методологии исследования, позволяя вашему субъективному мнению влиять на их результаты. Например, если взять ваш ответ:

Разницу между понятиями конечности и фиксированности определяет не "стандарт C++", а толковый словарь.

При минимамльном приближении можно понять, что толковый словарь, какой бы именно вы не имели ввиду (прим. толковый словарь Даля, Ожигова), не определяет разницу между конечностью и фиксированностью памяти системного стека. Понятия конечности и фиксированности памяти системного стека может определять только спецификация языка/компилятора/системы, в рамках которой этот стек и определён.

Пардон, я опечатался:

Не надо преувеличивать. Вовсе не бесконечное.

Извините, я боюсь, что вы не прочитали этот пункт до конца. Про то что стек не бесконечный Про то, что количество динамической памяти - не бесконечно я рассказал в двух абзацах, следующих сразу за процитированным вами.

На современных платформах размер стека фиксирован

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

Я исправил формулировку на более однозначную, благодарю за помощь!

Размер стека не фиксирован, а конечен ..
.. [поэтому] Утверждение про невозможность бесконечной рекурсии правильное, а его обоснование - нет.

Извините, можете, пожалуйста, подтвердить свою точку зрения ссылкой на стандарт С++ (или спецификацию компилятора, вроде GCC/MSVC), где определена разница между понятиями конечности и фиксированности размера системного стека, и где приведена причинно-следственная связь этих понятий с переполнением стека из-за бесконечной рекурсии?

Насколько я знаю, это субективные понятия, которые, соответственно, могут интерпретироваться каждым по-разному.

Поэтому, дорогой коллега, прошу вас, пожалуйста, для подкрепления вашего субъективного мнения, привести ссылки на более объективные источники информации, вроде: стандарта С++, спецификации компиляторов, спецификации ABI (например Itanium), или же результатов исследований, следующих научной методологии.

Если не разбираться в таких вопросах, то зачем тогда вообще программировать на C\C++ ? Берите Java или там C# - толку больше будет.

Извините, но, субьективно, это высказывание мне не кажется конструктивным, особенно по отношению к новичкам.

Во-первых,

Чтобы его сделать его более объективным, вам нужно сопроводить своё высказывание градацией "важных внутренних механизмов языка С++", категоризированной по разным областям бизнеса, по которой будет видно важность каждого конкретного нюанса, и как знание о них влияет на бизнес. На вскидку, даже если бы я занялся такой категоризацией, субъективно, я бы не отнёс знание поднаготной ABI к мастхев знаниям для младшего разработчика. Кажется, что даже для множества разработчиков уровня middle и выше профит от этих знаний довольно эфимерный.

Во-вторых,

Я думаю, что, даже если это и возможно, выучить детали всех внутренних механизмов работы С++, то всё ещё невозможно постоянно держать их в голове и эффективно при этом работать. Потому что эти знания уходят корнями в бесчисленные нюансы работы ОС, о которых, в связи со сложностью устройства современных ОС, невозможно знать всё, заранее и сразу.

Поэтому,

Я думаю, что это хорошо для общего понимания - знать теорию, и как С++ работает "под капотом". Представление же о мире вида: "без знания всех этих нюансов у человека не получится написать ничего путного на С++", с моей точки зрения, не имеет ничего общего с реальностью.

Более того я считаю, что такой подход - деструктивен, он не ведёт ни к чему кроме разочаровния в бесконечной погоне за недостижимым "абсолютным знанием", в бесконечном цикле изучения "основных-основ" вкупе со штрудированем Александреску, The C Programming Language, и тонкостей языка Assembly.

Здравствуйте!

На современных платформах размер стека фиксирован только в смысле резервирования адресного пространства под него...

Подскажите, пожалуйста, как это отменяет исходное высказывание?
Размер стека всё ещё фиксирован, и бесконечное количество вызовов всё ещё приведёт к рекурсии.

Не надо преувеличивать. Вовсе не бесконечное.

Извините, я боюсь, что вы не прочитали этот пункт до конца. Про то что стек не бесконечный я рассказал в двух абзацах, следующих сразу за процитированным вами.

Да, вам нужно выбрать нужный язык в выпадающем списке:

Здравствуйте! Спасибо за статью!
Небольшой совет, для улучшения читаемости код лучше поместить в специальный блок:

Здравствуйте!

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

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

Теперь, говоря о доказательной базе.

Например, есть спецификация Common Vendor ABI (Itanium C++ ABI), которую некоторые вендоры поддерживают в своих компиляторах.

Это попытка решить на уровне этой спецификации многие вопросы ABI совместимости, для которых стандарт оставил пространство для воображения.

По этому поводу даже был purposal WG21 N4028 Defining a Portable C++ ABI, но, судя по всему, он не был удовлетворён. В связи с чем, каждый компилятор горазд на свою имплементацию стандарта, в частности каждый сам решает хотят они поддерживать Common Vendor ABI (Itanium C++ ABI), или нет: GCC, к примеру, начиная с 3 версии, использует имплементацию Itanium:

... Furthermore, C++ source that is compiled into object files is transformed by the compiler: it arranges objects with specific alignment and in a particular layout, mangling names according to a well-defined algorithm, has specific arrangements for the support of virtual functions, etc. These details are defined as the compiler Application Binary Interface, or ABI. From GCC version 3 onwards the GNU C++ compiler uses an industry-standard C++ ABI, the Itanium C++ ABI.

Но жизнь в GCC есть и до его третьей версии, поэтому, даже при использовании только GCC, но разных его версий, мы можем увидеть странное поведение при переходе бинарной границы из-за разницы имплементаций.

Что конкретно может приводить к несовместимости?

Согласно Itanium Level II: C++ ABI 2.1 Introduction, стандарт С++ не настоял на следующих деталях:

The second level of specification is the minimum required to allow interoperability in the sense described above. This level requires agreement on:

  • Standard runtime initialization, e.g. pre-allocation of space for out-of-memory exceptions.

  • The layout of the exception object created by a throw and processed by a catch clause.

  • When and how the exception object is allocated and destroyed.

  • The API of the personality routine, i.e. the parameters passed to it, the logical actions it performs, and any results it returns (either function results to indicate success, failure, or continue, or changes in global or exception object state), for both the phase 1 handler search and the phase 2 cleanup/unwind.

  • How control is ultimately transferred back to the user program at a catch clause or other resumption point. That is, will the last personality routine transfer control directly to the user code resumption point, or will it return information to the runtime allowing the latter to do so?

  • Multithreading behavior.

  • The layout of the exception object created by a throw and processed by a catch clause.

  • When and how the exception object is allocated and destroyed.

  • The API of the personality routine, i.e. the parameters passed to it, the logical actions it performs, and any results it returns (either function results to indicate success, failure, or continue, or changes in global or exception object state), for both the phase 1 handler search and the phase 2 cleanup/unwind.

  • How control is ultimately transferred back to the user program at a catch clause or other resumption point. That is, will the last personality routine transfer control directly to the user code resumption point, or will it return information to the runtime allowing the latter to do so?

  • Multithreading behavior.

Разница может появиться из-за разницы механизмов:

  1. Раскрутки стека


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

    Вероятные последствия: нарушение целостности системного стека, UB с иногда стреляющим SIGSEGV/SIGBUS.

  2. Создания и перехвата исключений

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

    Вероятные последствия: нарушение целостности памяти программы, UB с иногда стреляющим SIGSEGV/SIGBUS.

  3. Лайаута исключений

    Расположение объектов исключений в памяти (порядок полей, выравнивание, и т.д.) может различаться в зависимости от компилятора.

    Вероятные последствия: UB с иногда стреляющим SIGSEGV/SIGBUS.

  4. Менглинга имён

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

    Вероятные последствия: в зависимости от имплементации, мы можем получить ODR violation и соответствующий UB.


Выглядит так, что у вас достаточно материала, чтобы написать статью на эту тему :)

Щас бы чатгпт использовать для ответа

Не понимаю как ваша реакция связана с объективной реальностью. Если вы готовы продолжать дискуссию, то прошу указать в чём я был не прав :)

P.P.S в статье раз 7 фигурирует std::move на const char*, видимо опечатка

Спасибо! Исправил.

Прошу прощения, я выразился не до конца ясно. Я думаю, что основная причина успеха вашего экспиримента скрывается в том, что вы используете одну и ту же платформу с одним и тем же компилятором.

При минимальном рассмотрении теория о том, что copy elision будет поддреживается в общем случае, представяется несостоятельной. Мои рассуждения, в результате которых я делаю такой вывод, следующие:

Во-первых,

Copy elision, как и любая другая опитимизация, описан в стандарте только своими эффектами (то что должно произойти с уровня абстракции языка), но стандарт не регламенитрует конкретную реализацию (то как имплементация конкретно должна это делать).

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

В частности,

Вызывающий функцию модуль, как и модуль, содержащий реализацию функции, всегда может быть собран со стандартом меньшим, чем С++17, например С++11/C++14. Поэтому компилятор, при сборке этого модуля, согласно стандарту С++11 и C++14, не обязан поддерживать copy elision.

Мой вывод:

Это implementation defined поведение, на которое я бы не стал завязываться, если вас интересует вопрос совместимости двух, потенциально собранных разными инструментами, модулей.

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

Извините, ваше утверждение основано на эксперименте с одним компилятором в одном конкретном окружении. Если вы претендуете на научный подход, то, вам нужна рандомизированная выборка, или ссылка на стандарт.

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

Прошу, пожалуйста, обратить внимание:

Поэтому, в общем случае, RVO и NRVO при пересечении бинарной границы между разными модулями - невозможны.

Очень интересный подход, спасибо, что поделились! И правда, если решать обработку ошибок на уровне системы, то проблем убудет. Это случайно не ОС под встроенную систему? От такого подхода с отказом от исключений на уровне системы немного веет программированием под встроенные системы.

В одной компании я увидел, как они реализовали свой механизм полиморфизма подтипов, в связи с чем они могли без зазрения совести делать вызовы "виртуальных" методов через бинарную границу. Но у этой компании и свой полноценный аналог STL был, со всякими дополнительными плюшками вида встроенной во фреймворк сериализации. Да и огромной она была, компания эта, деньги на разработку и необходимость в этих инструментах у них точно были.

Вцелом, я думаю, что такие подходы с переписыванием стандартных механизмов - очень дорогие, в связи с чем мало кто может себе такое позволить. И рассматривание таких механизмов - очень узкоспециализированная деятельность, далеко выходящая за рамки моего overview.

Информация

В рейтинге
Не участвует
Откуда
München, Bayern, Германия
Зарегистрирован
Активность

Специализация

Software Developer, Application Developer
C#
C++
Multiple thread
C++ Boost
Git
High-loaded systems
Rust