По мере роста кодовой базы проекта появляется смысл в распределении фрагментов исходного кода по отдельным файлам. Это позволит сократить накладные расходы на его сопровождение и упростить модификацию и доработку кода в будущем.
При этом распределение кода программы по множеству файлов затрудняет процесс компиляции. Обычная команда для компиляции заменяется на последовательность команд.
$ gcc hello.c hellomain.c
При передаче множества имен файлов компилятор самостоятельно вызывает линковщик для связывания объектных файлов в результирующий исполняемый файл.
Альтернативным решением, используемым при сборке сложных проектов, является компиляция отдельных файлов исходного кода с последующим связыванием объектных файлов.
$ gcc -c hello.c -o hello.o
$ gcc -c hellomain.c -o hellomain.o
$ gcc hello.o hellomain.o -o hello
Преимущество такого подхода заключается в том, что при внесении модификаций в кодовую базу проекта приходится перекомпилировать лишь изменившиеся файлы исходного кода. В случае сложных проектов, сборка которых занимает достаточно много времени, это обстоятельство помогает сэкономить достаточно много времени. В то же время, сам процесс сборки значительно усложняется и требует особого внимания.
Компиляция каждого из файлов исходного кода в ручном режиме может значительно усложниться ввиду увеличения количества таких файлов в проекте. Для этой цели был разработан инструментарий Meson, позволяющий осуществлять сборку проектов в автоматическом режиме. Он использует файлы с описанием правил сборки с именами meson.build. Сама сборка осуществляется в отдельной директории силами утилиты ninja в параллельном режиме.
Рассмотрим простейший файла meson.build для сборки простой рассмотренной ранее программы. Для начала рассмотрим код самой программы.
Это содержимое файла исходного кода hello.c:#include <stdio.h>
#include «config.h»
void print_hello(void)
{
}
Загрузить исходный код примера
Это — содержимое файла исходного кода hello.h:
#ifndef HELLO_H
#define HELLO_H
void print_hello(void);
#endif /*HELLO_H*/
Загрузить исходный код примера
А это — содержимое файла исходного кода hellomain.c:
#include <stdio.h>
#include «hello.h»
int main()
{
print_hello();
return 0;
}
Загрузить исходный код примера
Это файл для передачи параметров конфигурации от системы сборки приложению (config.h.meson). На этапе конфигурации сборочного окружения будут установлены значения описанных констант.
#ifndef __CONFIG_H__
#define __CONFIG_H__
#mesondefine CONFIG_NAME
#mesondefine CONFIG_VERSION
#endif // __CONFIG_H__
А это файл meson.build. Зависимость от библиотеки glib приведена для демонстрации и не имеет практического значения.
project(‘Hello’, ‘c’,
version: ‘0.0.1’,
license : ‘GPL3+’,
meson_version: ‘>=0.31’)
glib = dependency(‘glib-2.0’, version: ‘>=2.32’)
config_h = configuration_data()
config_h.set(‘CONFIG_NAME’, ‘»@0@»‘.format(meson.project_name()))
config_h.set(‘CONFIG_VERSION’, ‘»@0@»‘.format(meson.project_version()))
configure_file(input: ‘config.h.meson’,
output: ‘config.h’,
configuration: config_h)
c_sources = [
‘hellomain.c’,
‘hello.c’,
]
hello = executable(‘hello’,
c_sources,
install: true,
dependencies : [glib])
Загрузить исходный код примера
В данном случае функция project() используется для задания параметров проекта, а именно, его имени (‘Hello’), языка программирования (‘c’), версии (version: ‘0.0.1’), лицензии (license: ‘GPL3+’) и минимальной версии Meson (meson_version: ‘>=0.31’
). Функция dependency() используется для поиска зависимостей с помощью pkg-config и возвращает объекты найденных библиотек для последующего связывания в рамках функции executable(). Функция executable() задает параметры результирующего исполняемого файла, а именно, его имя (‘hello’), список файлов исходного кода (‘c_sources’), указание на необходимость установки (install: true) и список связываемых библиотек (dependencies: [glib]).Для сборки проекта достаточно вызвать утилиту meson в директории с файлами исходного кода.
$ meson build
После этого нужно перейти в созданную директорию build и выполнить команду ninja.
$ cd build
$ ninja
Установка в систему осуществляется с помощью отдельной команды.
$ ninja install
linux-faq.ru
Может кто-нибудь объяснить концепцию компиляции программы C для конкретной архитектуры?
Да. Идея состоит в том, чтобы перевести C в последовательность нативных машинных команд, которые кодируются программой в двоичную форму. Значение «архитектуры» здесь — «архитектура набора инструкций», то есть как инструкции кодируются в двоичном формате. Например, каждая архитектура имеет свой собственный способ кодирования для команды, которая добавляет два целых числа.
Причиной компиляции машинных команд является то, что они работают очень быстро.
Если одна программа использует определенную архитектуру, все библиотеки, которые она загружает, должны быть в одной и той же архитектуре?
Да. (Исключения существуют, но они редки.)
Как я могу определить, в какой архитектуре работает данная программа/процесс?
Если процесс выполняется на вашем оборудовании, он запускается на собственной архитектуре, которую можно найти в Unix, выполнив команду uname -m
, хотя для читателя для человека вывод из uname -a
может быть более информативным.
Если у вас есть исполняемая двоичная или разделяемая библиотека (.so файл), вы можете обнаружить ее архитектуру с помощью команды file
:
% file /lib/libm-2.10.2.so /lib/libm-2.10.2.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped % file /bin/ls /bin/ls: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.8, stripped
Вы можете видеть, что эти двоичные файлы были скомпилированы для очень старой архитектуры 80386, хотя мое оборудование является более современным i686. I686 (Pentium Pro) обратно совместим с 80386 и запускает бинарные файлы 80386, а также собственные двоичные файлы. Чтобы сделать эту обратную совместимость возможной, Intel столкнулась с большими проблемами и расходами, но они практически загнали рынок настольных процессоров, поэтому он того стоил!
qaru.site
Хотя старый вопрос я хотел бы добавить здесь 5cents, так как я, вероятно, не единственный, кто находит этот вопрос через поисковую систему.
Я не могу комментировать скорость компиляции, но на скорости выполнения:
Насколько мне известно, в С++ есть только одна особенность, которая стоит производительности, даже если вы ее не используете. Эта особенность — это исключения С++, поскольку они предотвращают несколько оптимизаций компилятора (вот почему, почему noexcept
был введен в С++ 11). Однако, если вы используете какой-то механизм проверки ошибок, то исключения, вероятно, более эффективны, чем комбинация проверки возвращаемого значения и много операторов if else
. Это особенно актуально, если вам нужно эскалации ошибки в стеке.
В любом случае, если вы отключите исключения во время компиляции, С++ не вводит никаких накладных расходов, кроме случаев, когда вы намеренно используете связанные функции (например, вам не нужно платить за полиморфизм, если вы не используете виртуальные функции), тогда как большинство функций не вносят никаких издержек во время выполнения (перегрузка, шаблоны, пространства имен aso). С другой стороны, большинство форм общего кода будет намного быстрее в С++, чем эквивалент в c, потому что С++ предоставляет встроенные механизмы (шаблоны и классы) для этого. Типичным примером является c qsort vs С++ std:: sort. Версия С++ обычно намного быстрее, поскольку внутри сортировки используется функция компаратора во время компиляции, которая, по меньшей мере, сохраняет вызов через поиск функции и в лучшем случае позволяет много дополнительных оптимизаций компилятора.
Как говорится, «проблема» с С++ заключается в том, что легко скрыть сложность от пользователя, так что казалось бы, невинный код может быть намного медленнее, чем ожидалось. Это происходит главным образом из-за перегрузки оператора, полиморфизма и конструкторов/деструкторов, но даже простой вызов функции-члена скрывает прошедший this
-поток, который также не является NOP.
Учитывая перегрузку оператора: когда вы видите *
в c, вы знаете, что это (на большинстве архитектур) одна, дешевая инструкция ассемблера, в С++, с другой стороны, это может быть сложный вызов функции (подумайте о матричном умножении). Это не значит, что вы можете реализовать ту же функциональность в c быстрее, но на С++ вы прямо не видите, что это может быть дорогостоящая операция.
Деструкторы аналогичны: в «современном» С++ вы вряд ли увидите какие-либо явные разрушения с помощью delete, но любая локальная переменная, выходящая за пределы области видимости, потенциально может вызвать дорогостоящий вызов (виртуального) деструктора без одной строки кода, указывающей это (конечно, игнорируя }
).
И, наконец, некоторые люди (особенно из Java) склонны писать сложные иерархии классов с множеством виртуальных функций, где каждый вызов такой функции является скрытым вызовом косвенных функций, который трудно или невозможно оптимизировать.
Поэтому, в то время как скрыть сложность от программиста, как правило, это хорошо, что иногда оказывает неблагоприятное влияние на время выполнения, если программист не знает о расходах на эти «простые в использовании» конструкции.
В качестве резюме я бы сказал, что С++ упрощает для неопытных программистов писать медленный код (потому что они напрямую не видят неэффективности в программе). Но С++ также позволяет хорошим программистам писать «хороший», правильный и быстрый код быстрее, чем с помощью c, что дает им больше времени для размышлений об оптимизации, когда они действительно необходимы.
P.S.:
Две вещи, о которых я не упоминал (возможно, среди других, которые я просто забыл), — это способность С++ для сложных вычислений времени компиляции (благодаря шаблонам и constexpr) и c ограничить ключевое слово. Это связано с тем, что пока не использовалось ни одно из них во временных критических программах, и поэтому я не могу комментировать их общую полезность и реальную выгоду в плане производительности.
qaru.site