Как стать автором
Обновить

Комментарии 94

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

Ну разве что это полезно в очень специфичных случаях когда релоки могут являться узким местом в производительности.
Даже в самом супер-пупер современном Xeon'е 7300-й серии кеш первого уровня - 32KB, кеш второго уровная - 2MB на ядро. Так что бывают случаи когда и лишние 10KB - большое дело. Если у тебя из-за этих 10KB данные не лезут в кеш. В случаях когда таблица велика - можно получить не пару десятков килобайт экономии, а пару мегабайт. А пара мег - это уже задержка в 0.02секунды при чтении с диска. А если такая библиотека не одна ?

В конце-концов зачем вы пишите на C/C++ ? Из любви к исскуству ? Я как-то привык считать что из соображений эффективности (если скорость работы программы не так критична - есть Java, C#, Python, Ruby, etc)..
То есть этот прием позволяет выиграть пару секунд при загрузке приложения, использующего несколько десятков либ по несколько миллионов статических строк в каждой? Довольно специфичный случай.

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

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

Что касается C++ — я лично на нем пишу из любви к деньгам, по другому не получается пока :)

Однако ж, стремиться к высокой производительности — совсем не значит тратить время чтобы достичь микроскопической экономии памяти.

Насчет кэша я не очень понял, как наличие релоков может на него как-то влиять. Ну да, загрузчик там будет патчить адреса в .data — но ведь после того как либа загружена в память, разницы между #2 и #3 не будет вообще никакой: 40K табличка указателей + 80K табличка строк. Так что я не понимаю, засчет чего оно медленнее получилось у вас.
Даже если не учитавать разницу в размерах (обратите внимание на последнюю строку последней таблички) варианты различаются:
вариант #2: 80K табличка строк + N*40K табличек указателей (где N - число одновременно работающих программ)
вариант #3: 80K табличка строк + 40K табличек указателей (независимо от количества программ)
Давно вы с системой на которой работает только одна программа сталкивались ?
Не, стоп — N это число программ, одновременно загружающих в память нашу библиотеку. С библиотеками которые использует только одна программа я довольно часто встречаюсь, да.
Нафига нужна библиотека, которую пользует одна программа ? Включите библиотеку в программу - и дело с концом. В большинстве случаев код ускорится ибо все манипуляции с GOT исчезнут. А если библиотека оформлена как отдельная сущность, то рано или поздно появится вторая программа, которая будет её использовать...
Чтобы уменьшить время линковки, чтоб юзеру посылать одну пропатченную библиотеку а не всё сразу. Потом часто бывает что библиотека используется *в основном* только одной программой.

Ну у нас тут есть таки некоторые классовые различия, т.к. я под виндовс пишу, но тем не менее и под линуксом N, я уверен, слишком мало чтобы его учитывать. И собственно программ, использующих технологию "а давайте все строки намертво зашьем в гигантский статический массив" тоже крайне невелико.
Для меня как-то библиотека без исходников - дикость, да. Иногда приходится такое пользовать (если уж совсем край), но обычно проблем в том чтобы слинковать всё в одну программу нет. Даже если в один бинарник влинковывается сотня библиотек общим объёмом в гиг - процесс не занимает так уж много времени. Время линковки гораздо лучше уменьшать путём уменьшения количества экспортированных функций... static (anonymous namespace в C++) - это наше всё...

А вообще подобного рода массивов в разных программах больше чем кажется на первый взкляд. Но, конечно, руками они редко порождаются - обычно это результат конвертации чего-либо в C/C++. И вот как раз при написании соответвующих конверторов эту информацию бывает полезно учесть: конвертор пишется один раз, а порождать может десятки и сотни таблиц...
Во, отлично: конвертор нормальный юз кейс, так как тот код все равно руками никто трогать не будет, и любые ужасные макросы там простительны ;)

Насчет линковки смысл такой — *уже* имея огромных размеров приложение на C++, его проще всего попилить на динамические библиотеки. Глубоко рефакторить, уменьшая зависимости, может быть довольно долго, дорого и нерационально.
А если библиотека оформлена как отдельная сущность, то рано или поздно появится вторая программа, которая будет её использовать...

Это если библиотека хорошая. Это раз. А два - это то, что библиотека в подобном виде неудобна. Гораздо экономичнее (по скорости загрузки) будет подгружать нужные строки из файла по мере их необходимости. Это не сложно запрограммировать. И это позволяет более гибко использовать библиотеку. И это экономит адресное пространство процесса.
Это если библиотека хорошая. А если она плохая - то она должна умереть (что со временем и произойдёт). Числа один в каком-то смысле нет: есть ноль и много. Есть, конечно, исключения, но если у вас их десятки - то что-то с вашим стилем разработки не так.

А два - это то, что библиотека в подобном виде неудобна. Этого поссажа не понял совершенно: хранение набора строк - это внутреннее дело библиотеки, как это вообще влияет на её удобство ???

Гораздо экономичнее (по скорости загрузки) будет подгружать нужные строки из файла по мере их необходимости. На время загрузки размер таблицы (равно как и её наличие) не влияет практически никак. Но зато загрузка из файла создаёт копию этих строк в каждом процессе, который вашу библиотеку использует. Можно вывернуться через mmap, но это уже потребует от вас гораздо больших усилий и насчёт это не сложно запрограммировать появляются сомнения (mmap имеет много разных тонкостей). Экономия же адресного пространства - это всё-таки что-то из прошлого: в 16-битной Windows 1.0-3.1 я понимаю почему это было важно, но какая же у вас должна быть таблицы чтобы на современных 64-битных системах экономия адресного пространства стала важной ???

Да, иногда вынести подобную таблицу из библиотеки имеет смысл, но далееко не всегда. И тут нужно долго взвешивать "за" и "против": кроме адресного пространства есть ещё такая банальная вещь как оперативная памяти и кеши процессоров...
Тык а зачем огород городить для современной 64-битной системы? Да ещё с современным жёстким диском, да ещё и с современной памятью?

Но зато загрузка из файла создаёт копию этих строк в каждом процессе, который вашу библиотеку использует.

Это только в том случае, если всем процессам всегда нужны все эти строки. Но такой сюжет - редкость (imho).

Так же не понятно, а зачем должно быть так, чтобы вся эта таблица влезала в кэш? При каком сценарии работы такое необходимо?
gperf->in_word_set. Вам рассказать - где gperf используется и зачем ? Или сами догадаетесь ? И почему порождённая им таблица, сидящая в библиотеке, может как активно использоваться так и совсем не использоваться (в зависимости от клиента) ?

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

P.S. gperf порождает подобную таблицу при использовании ключика -P...
Я не говорю, что оно никогда не должно использоваться. Пытаюсь только понять, в каких случаях и для чего его можно использовать.

И обращение к тому, что gpref так делает нисколько меня не убеждает. Ну, делает и делает. Это не означает, что нет способа лучше. Эмс. И даже не понятно, зачем вообще какая-то таблица из строк строится, если это структура для поиска. Конечные автоматы и словари уже не в моде?

Да и вообще, необходимость в подобной структуре с 1000 ключевых слов совершенно неясна. Зачем такое может быть нужно? 1000 различных ошибок - это ещё с натяжкой, но понятно. А 1000 токенов... Это я себе не представляю, уж простите мне мой ограниченный разум.
НЛО прилетело и опубликовало эту надпись здесь
Java на самом деле "рвёт в клочья" С++ на многих бенчмарках и, как правило, "сливает" в реальной жизни как раз из-за того, что JIT и собственно сама программа делят процессор и его жалкий 32KB L1 кеш. Без JIT'а - Java и C++ даже сравнивать не стоит. Да, есть случаи когда Java оказывается даже быстрее С++ в реальной жизни, но если вы с таким сталкивались - расскажите, интересно. Я - не сталкивался... В тех случаях когда всё статическое (то есть JIT может всё отоптимизировать и успокоиться) - к С++ можно прикрутить profile-based оптимизацию и Java опять останется "не при делах"...
Г-н тролль говорил о "сопоставимой скорости", а не "быстрее" (:
И еще, чисто теоретически PBO таки меньше возможностей имеет, чем JIT.
JIT имеет возможность обойти PBO в теории там где ситуация динамически меняется и данные PBO оказываются неадекватны - но в этой ситуации у нас как раз и возникает борьба за кеш процессора! В теории иногда JIT всё же должен выходить победителем - но на практике я такого не видел...
хотелось бы посмотреть на тестирование производительности
C++ vs Java vs C#

Я вот тут нашел что-то, но как-то не очень наглядно:
http://www.tommti-systems.de/main-Dateien/reviews/languages/benchmarks.html
что-то не найду тестов под win...
А некоторые знают, что ассемблер не так быстр, как многие полагают… О_о
Один код на асме может переводится в несколько разных машинных кодов, и компилятор очень редко угадывает что оптимально…
Пример?
Язык Си объединяет мощность ассемблера, с одной стороны, и скорость ассемблера, с другой :)
© профессор информатики
С/С++ Компиляторы сейчас оптимизируют код гораздо лучше, чем это мог бы сделать человек на ассемблере. Учитывается размеры кэша, количество потоков в процессоре и т.п.
(недавно это проходили на спецкурсе "Параллельные вычисления"
Граница проходит где-то между x86 и x86-64 из-за пресловутого "правила 7 объектов". Человек может с большой ловкостью манипулировать 8ю регистрами x86, а тупые алгоритмы в компиляторе с этим часто не справляются, в то время как 16 регистров x86-64 уже плохо помещаются в голове... Про 32 регистра POWER'а и тем более 32+96 Itanic'а я вообще молчу...

Это не значит что компилятор не может сделать что-то быстрее сгенерированного компилятором кода, но он может это сделать в очень ограничеснных масштабах (типа: маленькая процедурка за месяц работы)...
Тьфу. Человек может сделать что-то быстрее сгенерированного компилятором кода в ограниченных масштабах, конечно :-)
маленькая процедурка за месяц работы
Насколько маленькая? ;)
Раньше люди писали и не плакали.
вот, например, Frontier (http://www.frontier.co.uk/games/frontier) : 250'000 строчек 68к ассемблера.

В чём проблема заюзать регистры? На CELL я могу загрузить тупо 8 матриц в регистры и использовать оставшиеся 96 регистров для раскрутки цикла. Т.о. проблемы нет.
Да, если хочется показать что я - типа мегакулхацкер, могу вон какие штуки на ассемблере писать - это сейчас легче чем 10 лет назад. А вот сделать что-то, что на ассемблере быстрее, чем на C/C++ с PBO - это другой вопрос.

Ибо написать на асме миллион строк - небольшая проблема. Но смысл ? Получить все недостатки C/C++ и ассемблера в одном флаконе ? Если уж писать что-то на ассемблере, то это что-то должно быть заметно лучше, чем то, что порождает компилятор (иначе какой смысл?). И вот это-то сделать весьма и весьма непросто: я уверен что и frontier и многие другие подобные же примеры можно было ускорить переписав их на C/C++ и применив PBO. Раньше компиляторы были глупее, не обгонял их только ленивый. Сейчас - это весьма непросто если только твой алгоритм не относится к числу "особо нелюбимых" компиляторами (как криптуха).

Маленькая - скажем AES в 256 байт: у меня знакомый в какой-то момент для embedded системы да ещё и под не очень популярный процессор писал (нужен был минимальный код, не минимальная скорость - уже с этим современные компиляторы не очень хорошо справляются). Заняло это примерно месяц как раз :-)

Проблема в том что ты загрузишь тупо 8 матриц в регистры, используешь остальные 96 для раскрутки цикла - и в результате потеряешь время на загрузке/выгрузке всего этого массива в стек (твоя же программа не состоит только из операции перемножения матриц!). А компилятор на основе профайла аккуратненько посчитает - насколько именно нужно развернуть цикл и какие именно регистры для чего использовать надо. И получит, скорее всего, более быстрый код. Ибо всё на свете никогда в регистры не упихаешь, а когда тех регистров - не 4 и не 8, то уже много времени уходит на их сохранение/восстановление, а посчитать сколько конкнкретно - ой как непросто...
в результате потеряешь время на загрузке/выгрузке всего этого массива в стек
Это достаточно сделать 1 раз. Ассемблерная программа не скована правилами ABI.

Ибо всё на свете никогда в регистры не упихаешь
Упихаешь то что нужно. Компилятор не знает, то что _мне_ нужно.
Я пишу на простом, понятном железе без OoO - CELL SPU.

Вы слишком хорошего мнения о компиляторах. Сейчас я пишу на интринсиках gcc - пишу и плачу. Плачу потому, что на асме я бы сделал более интересный код, но пока что у меня приоритет смещён в сторону правильных параллельных алгоритмов, а не их оптимизации.
Написание параллельного кода - это пока что не вотчина компиляторов. По разным причинам под его написание их никто не затачивал. Наиболее продвинутый в этом смысле компилятор, насколько я знаю, это ICC, но его под CELL не поиспользуешь...
Эт бесспорно, просто человек там выше явно что-то другое имел ввиду.

Я из похожего могу только припомнить как турбо ассемблер переводил "shl reg, N" в N инструкций "shl reg, 1" поскольку до 286 процессора инструкции для побитового сдвига на N бит не было, только на единичку. %)
Unlike in high-level languages, there is usually a one-to-one correspondence between simple assembly statements and machine language instructions.
(см. полный текст).
usually - это «обычно», а не «всегда», так что...
IMHO, "usually" здесь написано исключительно потому, что чисто теоретически такой ассемблер можно написать. Но я за 20 лет с такими не сталкивался ни разу.

Вы можете привести пример лично Вам известного ассемблера, который поступает вопреки этому правилу?
вообще, похвально. Я тоже люблю бороться за такты процессора, хоть и эксперименты провожу редко когда больше чем на двух разных CPU. Да и на двух-то - ооочень редко =). Как правило, если в программе не единственная функциональность - та, которую вы описали - а есть ещё что-либо, то способов все ускорить/уменьшить есть уйма. Как правило, это, для начала, пересмотр алгоритмов, а уж потом борьба с elf форматом и его особенностями. Учитывая общие тенденции роста функциональности у программ - я сомневаюсь что может остаться время на пересмотр подобного рода вещей - ведь добавляется новая функциональность, которая редко когда сразу пишется идеально и тааак далеее...

Тем не менее, решение стоящее, если когда-либо будет необходимость, с удовольствием вспомню =). Спасибо.
можно нескромный вопрос кстати. Сколько времени вы на это потратили?
Часа три, причём большая часть ушла на борьбу с Хабром (не хотелось одноцветные листинги публиковать).
Пример с кодом ошибки как-то не сильно показателен: довольно редко программа выводит десять миллионов сообщений об ошибках.

А в боевых условиях вы эти методики сравнивали, если не секрет? Есть подозрение, что в большой программе, работающей со строками, эти два десятка тактов молниеносно потеряются на фоне печати тех же сообщений или конкатенации токенов. И уж точно кеш первого уровня будет вести себя совершенно по-другому: в нем на эту таблицу ошибок может и не хватить места.
Как уже говорилось решение это - как раз из боевых условий. Но в основном не из кода, писанного руками, а из кода, который генерится. Зачем тратить компьютерное время на построение таблиц, которые вообще, скорее всего не понядобятся ?

Как я уже говорил этот приём применяется в GlibC (не помню с какой версии), в libpcre (начиная с версии 7.4 - посмотрите на файл pcre_tables.c), etc. Нельзя сказать что применять его приходится каждый день, но если библиотека популярная - это стоящая оптимизация...
Интересно, спасибо.

Поделюсь возникшей мысль. Между вариантами 2 и 3 есть пренебрежительно малая разница в производительности и пропасть - в плане читаемости кода и его "поддержабилити". В среде, в которой множество девелоперов в течении несколько лет развивают некую коммерческую систему, очень важно писать ее так, чтобы код был предельно прозрачен и понятен.

Приходят новые люди, и важно, чтобы они быстрее смогли разобраться с кодом. Могут возникать "баги из прошлого", и скорость их исправления прямо зависит от того, насколько прозрачно писался код несколько лет назад. И в реальной жизни далеко не всегда мы можем требовать от "новых людей" такой высокой квалификации (экономика!), чтобы они могли с полуслова понимать сложно написанный код (как вариант 3).

Простые синтаксические конструкции - это большая ценность для большого продукта, на самом деле.
Тут вы правы, конечно. Но "понимабельность" - она очень сильно зависит от опыта: увидев первый раз подобную конструкцию я тоже немного прифигел, а когда смотришь на неё в 20й раз - уже не видишь в ней ничего сложного.

Применять описанный приём стоит в нескольких случаях:
1. Для действительно больших таблий.
2. Для популярных библиотек.
3. Для автоматически сгенерированного кода.
Вот именно что в 20й раз, а в большом проекте это очень дорого :)
Если код определения ошибки вызывается слишком часто, значит что то не то с программой. Т.е. в жизни, та же запись в log существенно будет больше по времени чем рассматриваемый пример, так что время потраченое на поиск строки никого уже не интересует.

Лучше бы показали, как пример, разницу между каким нибудь криптографическим преобразованием, которое на C/C++ написано и в реализации на ассемблере. Вот тут и будет видна разница.
Если бы вы внимательно читали статью то увидели что как раз в таких случаях самый популярный вариант #2 транжирит больше всего ресурсов.

Что касается криптования... У криптования, написанного на ассемблере, велик "вау-фактор", но практически применить тот же подход в других случаях вряд ли удастся. Гораздо полезнее менее специфические приёмы: например тут описано как ускорить перемножение матриц в 5 с лишним раз без использования ассемблера или MMX/SSE - причём описанные приёмы применимы и к другим программам (упоминаемое ускорение служит там примером, а не является целью статьи). К сожалению статья большая, полностью её перевести меня, наверное, не хватит...
Нет, это наверное Вы меня не поняли :-)

1. Время работы функции получения строки по ошибки никого абсолютно не волнует.
2. В приложении, которое может сгенерить 10000 ошибок - лишние 40 килобайт не важны.
3. Про шифрование - не совсем вау-фактор. Я видел реализацию SHA-256 на MMX. Она в несколько раз быстрее чем обычная, оптимизированная, на C.

В целом, о чем я хочу сказать: субьект для оптимизации выбран неудачно.
Ещё раз:
1. Время работы функции получения строки по ошибки никого абсолютно не волнует - и именно поэтому обсуждаемая оптимизация имеет смысл.
2. Приложение, которое потенциально может сгенерить 10000 ошибок - это может быть какой-нибудь несчастный апплет, который использует какую-нибудь здоровенную CORBA-библиотеку. Большую часть её он может не использовать, но ld-*.so будет настраивать все таблицы, которые там есть. Ну и кому это нужно ?
3. Многие вещи, касающиеся шифрования, 3D и т.п. с MMX и SSE выполняются быстрее - для этого MMX и SSE, собственно, разрабатывали. Но описывать как это делать - это задача для отдельной книжки (и их есть: вот тут, например). Если привести одну реализацию одной отдельной функции - от этого вряд ли кому будет польза кроме того, кому эта функция нужна...
Хм. А почему бы не поручить всё шаманство с оформлениями таблиц какому-нибудь библиотекарю, вроде ld? Эмс. Зачем человеку делать то, что может сделать машина? Создать статически прилинковываемую библиотеку с этими строчками и всё, делов-то. Не понятно, зачем это шаманство с макросами.
А вы вообще-то статью читали ? Это и поручены ld. Только вот сделать он с этим ничего не может - и спихивает эту задачу уже уже на ld-*.so. Который и решает эту проблему. Миллион раз на миллионах компьютеров. Совершенно согласен с вами: если постановка задачи стоит как "сделать что-то человеку" vs "сделать что-то компьютеру", то лучше человека от подобных обязанностей избавить. А вот если задача стоит как "сделать что-то человеку один раз" vs "сделать что-то компьютеру триллион раз" (как в данном случае), то выбор уже даалеко не так однозначен. Статически прилинкованная библиотека только усугубляет дело: вместо N копий таблиц указателей вы получаете ещё и N копий таблиц строк!

Конечно если всё это используется в одной программе и ровно один раз - тогда можно всё слинковать статически и не мучиться, но практика показывает что закладываться на такой вариант - как минимум недальновидно...
Как так не может? Может (или у меня руки кривые?). Но вы же в опциях компилятора сами написали, что вам нужна динамическая разделяемая библиотека да ещё с position independent code. А потом мужественно боретесь с этими опциями. Смысл борьбы не понятен, если честно. Если можно сразу без PIC и без shared всё сделать. Тем более, если вы гоняетесь за скоростью.
Это уже обсуждалось: без PIC и без shared вы получите N копий всего на свете. Если N==1 (то есть вы гоняетесь за скоростью ровно одного процесса на машине) - это выход. Если нет - то нет. Почему вы считатете оптимизацию под один частный случай важнее чем оптимизацию под все остальные варианты ?
Никаких копий никто создавать не будет, если у вас N процессов выполняют одну и ту же программу (я так понимаю, именно такой тест вы прогоняли). А если программы разные, то тут ещё смотреть надо, сколько копий и где будут созданы. У вас, конечно, реализация функции не зависит от того, по какому адресу будет размещена структура, но от этого зависит код самой функции. Даже если загрузчик сможет отреагировать на эту тонкость, у вас код функции и данные в одном .text сегменте.

Хорошо, если у вас новомодный процессор с адрессацией, относительно указателя на инструкцию. Тогда можно кидать эту библиотеку по любому адресу и всё будет нормально работать. Но, даже в (x86 без 64) такого режима нет и в полсотне других архитектур (а у некоторых даже и виртуальной памяти-то нет). И для них вопрос про одну копию библиотеки в памяти - это вопрос.
А если программы разные, то тут ещё смотреть надо, сколько копий и где будут созданы. Дык кто мешает-то ?
$ relinfo libempty.so libgen*.so
libempty.so: 5 relocations, 2 relative (40%), 2 PLT entries, 0 for local syms (0%), 0 users
libgen1.so: 5 relocations, 2 relative (40%), 2 PLT entries, 0 for local syms (0%), 0 users
libgen2.so: 10005 relocations, 10002 relative (99%), 2 PLT entries, 0 for local syms (0%), 0 users
libgen3.so: 5 relocations, 2 relative (40%), 2 PLT entries, 0 for local syms (0%), 0 users
Для сравнения я взял библиотеку состоящую из пустого файла. Архителктура - IA32...

Хорошо, если у вас новомодный процессор с адрессацией, относительно указателя на инструкцию. При чём тут это ? Вы вообще представляете как ELF устроен ? Одной из основных принципов в его создании была задача сделать так чтобы код функций не менялся при перемещении библиотеки по памяти и его можно было mmap'ить. Точки входа - да, точки входа приходится хранить в отдельной таблице. Указатели на строки ? Да ни в коем разе!
Кстати, можно в двух словах — неужели в ELF с этим настолько по-другому, не так как в COFF? Как же там релоки работают? Ну то есть, если в коде есть ссылка на статические данные, или там адрес процедуры — надо ж как-то наверное таки патчить его при загрузке?
Вообще я уже давал ссылку - могу дать ещё раз. Вкратце - там есть отдельная таблица куда выносятся все модифицируемые данные. Современный процессор это почти не напрягает, но это позволяет весь код делать немодифицируемым. Более того: в последниях версиях Linux'а большая часть программ работает в режиме когда в памяти нет модифицируемых участков кода: либо что-то mmap'ится из файлы в режиме "только для чтения" (и тогда это может быть и код в том числе), либо что-то не mmap'ится - тогда это не код (ибо просто аттрибут соотвествующий снят).
Так при чём тут указатель на строку? У вас там указатель на static структуру. Если структура отображается по разным адресам, то код либо в таблице переадресаций должен поискать его, либо должен использовать адресацию относительно указателя текущей инструкции. И ELF тут ни при чём, так давным давно уже делают.

Проблема только в том, что таблицу переадресаций, возможно, не выйдет везде разместить по одному и тому же адресу (кто его знает, какая структура будет у библиотек, которые использует программа). А если её нужно будет разместить по разным адресам, то имеем две копии кода. А раз так, то, скорее всего, две разные копии .text, в котором у вас строки забиты (возможно, я не прав, но возможно и прав. Тут уже надо исходники загрузчика смотреть).
Ни то, ни другое - используется адресация относительно сегмента данных (он хранится в отдельном регистре и нужен, в частности, для того чтобы вызывать другие функции - ибо там же хранятся адреса функций из других библиотек; просто в участке памяти, который можно модифицировать).

Ещё раз: сегмент text священен. В нем никто и никогда ничего не меняет - ни в варианте #1, ни в варианте #2, ни в варианте #3...
Сегменты в x86 - это то, что позволяет оптимизировать метод, не более. Но, ещё раз, не везде есть сегменты. А там, где их нет, либо GOT выходит сложной (трёхступенчатое получение адреса массива - это круто, конечно), либо нужно модифицировать .text. Не везде идут по первому пути, особенно во встраиваемых системах. Область применения ELF не ограничевается Linux.

Поэтому, я возвращаюсь к вопросу. К чему это шаманство с макросами, если хорошо оно работает только для мощных систем, которые неплохо сжуют и более понятный вариант кода?
Сегменты ELF не имеют никакого отношения к сегментам x86. Сегменты ELF есть везде, где есть ELF. В частности у меня на роутере с его 8MB flash, 32MB flash и 200Mhz не суперскалярном BCM3302 - там тот же ELF и те же сегменты. Адрес массива получается так: к значению регистра, в котором хранится адрес сегмента данных прибавить известное смещение. Всё. Это только на x86 приходится иногда извращаться. А в ARM - 16 регистров, в MIPS - 32. Один из них отдаётся под адрес сегмента - и не проблем.

И даже более мощные системы имеют чем заняться кроме как тратить время на плохо написанный код...
Ну вот, сегменты в x86 помогают организовывать эти таблицы. Кстати, полез в подробности, и мне не понятно, а как получается использовать один массив, чтобы записывать информацию о всех данных во всех библиотеках. Как избежать конфликтов?
Один массив на динамическую библиотеку - при пересечении границы библиотеки происходит перенастройка, но она выполняется очень быстро ибо библиотек немного (будем надеяться) и всё это сидит во всех мыслимых и немыслимых кешах.
Даже если все подобные массивы разные, то как на момент компиляции определить, где их размещать... Вобщем, туманно как-то всё. То есть, понятно, как это сделать, но не понятно, как это сделать без модификации .text . Разве только прыжками на код, который правит ссылки в регистрах.
Разве только прыжками на код, который правит ссылки в регистрах. Возьмите с полки пирожок! Там дальше подробно всё описано. Только этот код, опять-таки, один на библиотеку и не меняется - но он меняет таблицу с адресами точек куда нужно прыгать...
Пирожки не ем. Там всё подробно расписано для случая одной библиотеки. Но как всё устроено, когда библиотек несколько?

По идее, и jmp в случае многих библиотек не может помочь. Как сгенерировать разные цели для jmp, во время компиляции кода? Хорошо, это может быть косвенный переход, но как во время компиляции сгенерировать адрес для взятия этого перехода?

Не могу вычитать про этот момент. Ткните пальцем в номер страницы.

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

Уж не поэтому ли half-life 2 под Linux использует dll, вместо elf для работы с динамическими библиотеками?
Думаю что всё гораздо банальнее: им было проще использовать winelib, чем портировать всё целиком под Linux...
Всё происходит следующим образом?

При вызове функции из некоторого DSO, сначала происходит вызов заглушки, которая меняет один из регистров (ebx в ia-32) так, чтобы он указывал на таблицу ссылок, которая состоит из трёх частей. 1. GOT - адреса переменных из других модулей, 2. GOTOFF - адреса собственных static'ов. И 3. PLT - таблица адресов заглушек для функций, тех самых, которые меняют ebx при межмодульных вызовах?

То есть, постоянно тратим один регистр для хранения данных об адресах, а потом ещё кучу регистров для кэширования адресов.

Ну... Это лучше, чем моё неверное предположение о поиске. Но всё-равно, лишние чтения из памяти и регистры... Да ещё и память на таблицы. Дороговато.
В таблице есть ещё два компонента:
1. Все глобальные переменные
2. Все сгенерированные компилятором глобальные объекты (строка "Hello, World!", в функции, которая выводит сообщение на экран, хранится тоже там).

И да, вызов функции из другой библиотеки - стоит ненулевых денег. 9 тактов в режиме x86-64 на Core2 (за счёт суперскалярности "реальная работа" может при этом занять нулевое время)...

Но ELF - это "реальность, данная нам в ощущениях". Идти против его правил - всё равно что плевать против ветра. Вот я и показал - как писать программы сообразуясь с правилами ELF. Ситуация в Windows немного другая - я об этом где-то в самом начале статьи написал. Там этот феномен может быть заметен только при особых условиях (конкретно - когда приходится делать для библиотеки rebase; причём если эта проблема окажется реальной можно использовать rebase.exe и избежать её).
То есть, решение-то, конечно само по себе красивое, функциональное и всё такое прочее. Но... Хм. Ладно... Не буду сомневаться. Просто красивое решение и всё :)
Собственно сомневаться - оно всегда полезно. Как я уже заметил: разумнее всего подобные вещи использовать в автогенерированном коде. Хотя и в неавтогенерированном это может иметь смысл - особенно для больших таблиц...
Я конечно не знаю как в линуксе, но в винде .text точно загружается как copy-on-write — то есть когда загрузчик будет адреса править, копии в памяти создадутся только для тех страниц, которые реально изменились.
Так тоже, скорее всего. Но не везде есть виртуальная памть да ещё и с защитой на запись.
В Linux'е не так: он загружается вообще в режиме read-only на уровне ядра. Его никому (в том числе загрузчику) править не дадут.
Так данные можно править и таблицы смещений, они наверняка тоже copy on write.
Данные - copy-on-write, но в варианте #2 все данные в соотвествующей таблице требуют модификации - причём в момент загрузки.
2007 год на дворе.
Как тут уже заметили выше ручной ассемблер сейчас медленнее оптимизированного современными компиляторами кода. И только в редких случаях быстрее (можно глянуть в современные 3D движки, там асм где надо дает плюс).
Ваши оптимизации со строками представляют собой чисто академический интерес. Хотя нет. Это полезно на embedded для остального, имхо, интереса не представляет.
Вы все еще обрабатываете ошибки кодами возврата на C++ (не на Си)? Тогда мы идем к вам (может стоит перейти обратно на Си?). Не надо только с сарказмом замечать про 5-10% penalty на throw/try/catch. Поддержка кода большого проекта - это ключевое слово. Хотя конечно и с исключениями можно такого наворотить)
Именно - на дворе 2007й год. Год, в котором вышеупомянутые оптимизации со строками наконец-то попали в libpcre. Ибо машинок на которых процессор имеет частоту не в гигагерцы, а, дай бог, в сотни мегагерц - миллиарды (куда больше чем персоналок и серверов вместе взятых), а памяти на них - не вагон.

И да - мы всё ещё обрабатываем ошибки кодами возврата на C++ (не на Си). Именно потому что поддержка кода большого проекта - штука непростая и исключения тут помогают далеко не так хорошо, как вам кажется. Написание exception-safe кода отбирает немало времени и сил (не говоря уже о пресловутых 3-5% которые приходится платить за исключения только потому что они включены в компиляторе но не используются).

Для задач где скорость работы, расходуемая памяти и т.д. не так критичны - есть Java/Python/etc, где исключения вполне уместны ибо о максимальной скорости работы речь даже и не идёт...
>> Именно потому что поддержка кода большого проекта - штука непростая и исключения тут
помогают далеко не так хорошо, как вам кажется.
Мне не кажется. Я делаю такой вывод на основе собственного (может быть и небольшого) опыта
Там где мегагерцы, я уже заметил выше про embedded (там, естественно где не используется Java, что серьезно уменьшает количество таких машинок)
Из собственного опыта: если вам кажется, что ваша система обработки ошибок, основанная исключительно на исключениях выглядит элегантнее, чем обработка кодов возврата, то она явно не полная.
Угу. Хороший пример - Java: там и иерархия и автоматическая проверка обрабатываемости исключений в языке и всё такое прочее, а потом бах и XOPEN or SQL:2003 code identifying the exception... Приплыли...
Ну во-первых кодов возврата будет далеко не 10000. В критичных местах - да, GetLastError() - да. Но у меня нет большого массива описаний ошибок.
А во-вторых это холивар.
А в-третьих сделайте-ка мне, пожалуйста, вывод стека вызванных функций , произошедших до исключения, используя коды ошибок (переносимый). Это будет быстрее? Элегантнее? Удобочитаемее? :)
Я прочитал тучи таких холиваров (exceptions vs. return codes) поэтому я надеюсь что мы тут подобный не разведем :)
ХМ. 3. По-моему, это некое смешение двух этапов жизни программы - процесса отладки и процесса использования. Зачем мне в процессе использования видеть такой стек во время ошибки при открытии файла, вместо надписи: файл не найден? А для отладки это делается очень просто. В конце функции ставится вывод на консоль\файл\сокет\куда угодно (а не только туда, куда это предусмотрено runtime'мом исключений) if (error) { printf("function __FUNCTION__ failed\n"). Да и этого делать не нужно, вобщем-то. Когда есть отладчик.
Неправда ваша. В отладчик лезть при каждом чихе? Вы имеете билд сервер. На ночь запускается скрипт (или CruiseControl это делает, не важно), стягивает все что за день накоммитили, собирает все unit-tests, integration tests, начинает запуск всех тестов. Где то падает, она по мылу всем сообщает о падении, полный отчет, чей коммит завалил билд (это уже по возможности), в том числе и логи о том, где упало.
__FUNCTION__ - нестандартая директива, следовательно способ не переносимый.
__FUNCTUION__ Это не деректива, а макрос с именем функции. Ручками можно сделать. Не понятно, зачем лезть в отладчик, и не понятно, при чём тут исключения? Всё описанное выше можно сделать при помощи GDB и хоть ассемблер отлаживать.

Или имеется в виду какой-то нестандартный способ использования исключений, при котором они выдают промежуточные состояния программы?
Не совсем понятно как ручками?
Да это макрос, но к определенным стандартным макросам ANSI он отношения не имеет.
В отладчик бывает дороговато каждый раз лезть я про то и говорю (и делать это стоит как можно реже, а чаще думать над чем, что пишешь, но это к делу не относится :)), а ваша цитата:
>> Да и этого делать не нужно, вобщем-то. Когда есть отладчик.
Про исключения: нет, способ стандартный вполне (если вы про стандарт, а не про типичное использование исключений). Для реализации подобного способа см. boost.exception например.
P.S. дИректива.
Угу, директива. Можно перед началом каждой функции определить 'вручную', если компилятор не поддерживает. Простой скрипт на Perl с этим может помочь.

Отладчик - это не та штука, которая по шагам позволяет программу выполнять, когда программист нажимает определённую кнопочку. В моём понимании отладчик - это штука, которая собирает информацию о ходе выполнения программы. GDB, например. Отладку с ним можно организовать в описанном вами режиме.
ISO/IEC 9899:1999, §6.4.2.2:
Semantics
1. The identifier __func__ shall be implicitly declared by the translator as if,
immediately following the opening brace of each function definition, the declaration
        static const char __func__[] = "function-name";
appeared, where function-name is the name of the lexically-enclosing function.
2. This name is encoded as if the implicit declaration had been written in the source
character set and then translated into the execution character set as indicated in translation
phase 5.
3. EXAMPLE Consider the code fragment:
        #include <stdio.h>
        void myfunc(void)
        {
                printf("%s\n", __func__);
                /* ... */
        }
Each time the function is called, it will print to the standard output stream:
        myfunc
Так это в C99 только ;)

Но вообще конечно, в жизни, код, использующий (пускай нестандартный) макрос __FUNCTION__, намного более переносим чем использующий стандартные эксепшены :)
Разве C++ - это хорошо стандартизованый язык? Мы тут экспериментровали как-то, когда студентов учили использовать исключения, так под visual с/с++, g++ и borland c/c++ исключения обрабатывались тремя разными способами: разными были последовательности (с учётом количества) вызова конструкторов копирования и разными были последовательности прохода по catch. При этом качественно разными.

Лично я не представляю, как на C++ можно писать какие-нибудь важные и требовательные к скорости приложения.
Ну я о том же (в плане стандартов).

Хотя, что касается вашего случая, наверное это было давно — gcc и msvc практически на 100% сейчас стандарту соответствуют, за исключением совсем мелких мелочей (ну и export template, которого практически нигде нет и не собираются).
При этом качественно разными.

Хмм... А какой из них вёл себя не по стандарту ? Как раз с конструкторами копирования в случае обработки исключений стандарт позволяет весьма вольно поступать, но вот последовательность прохода про catch - здесь как-то странно всё... должно быть одинаково...
И какой же изврат) Неее, останусь на Python до лучших времён.))
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории