Сравнение производительности C и C++ на примере сжатия Хаффмана
Когда на IT-форумах задают вопрос «Быстрее ли язык программирования X языка Y», это обычно вызывает потоки эмоций и считается некорректным. Сродни вопросу про религию или предпочтение той или иной политической партии. Действительно, язык — это способ выражения мысли, идеи. В данном случае идеи программной системы. Он не быстр и не медлен. Он может быть более или менее лаконичным, более или менее точным. А скорость определяется не столько языком, сколько конечным кодом, который генерирует компилятор этого языка. Или скоростью интерпретатора в случае интерпретируемого языка.
Но это всё философия. А на практике обычно есть практическая задача разработки ПО. И, действительно, реализовать это ПО можно на десятке разных языков программирования. Поэтому, хоть это и «религиозный вопрос» в случае публичного обсуждения, вопрос этот часто возникает в голове IT-специалиста, стоящего перед конкретной задачей. «Сколько времени мне потребуется для реализации задачи на языке X и какие у полученного ПО будут характеристики, в том числе скоростные, по сравнению с реализацией этой задачи на языке Y». Понятное дело, точного ответа на этот вопрос нет, специалист опирается на свой личный опыт и отвечает как-то типа «с вероятностью 95%, написанная на ассемблере, эта задача будет работать быстрее, чем на php». Но, положа руку на сердце, опыт этот редко базируется на точных цифрах реальных задач, которые сам этот специалист реализовал. Нет, ну кто в здравом уме будет писать сложное ПО сначала на php, а потом его же переписывать на ассемблере, только чтобы измерить характеристики? В основном ограничиваются синтетическими тестами типа сортировки массива, построения и обхода бинарного дерева и тому подобных.
Я, как специалист, пишущий 90% на C++, часто натыкаюсь на «холливарные» темы сравнения этого языка с другими. И один из них — это прародитель — язык C. На том же quora.com часто поднимают этот вопрос «А быстрее ли язык C языка C++» (что некорректно, как я объяснил выше), или «А почему ядро Linux или тонна GNU утилит пишется на C а не на C++» (что является вполне корректным вопросом). На второй вопрос я для себя ответил так:
- Освоение языка C требует на порядок меньших усилий, значит, больше людей могут поучаствовать в разработке этого ПО.
- Сложные действия, потенциально затратные по памяти или скорости на языке C займут, вероятно, больше строчек кода и потребуют усилия от автора. А значит, неоптимальность в программе легче будет заметить по ходу написания или ревью. Программа на языке C++ может быть куда более лаконична и, с виду, проста в понимании. Но заметить, что за перегрузкой оператора «+», к примеру, скрывается запуск космического корабля к луне, заметить будет сложнее.
Задачка
Для того, чтобы чуть более основательно ответить себе на этот вопрос, я решил написать ещё один (да-да, тоже немного синтетический) тест — кодирование данных методом Хаффмана. Навела на мысль статья «Алгоритм Хаффмана на пальцах» (https://habrahabr.ru/post/144200/).
Сначала я реализовал кодирование на чистом C. Если помните, для его реализации требуется очередь с приоритетом, потому как там для построения дерева кодирования нужно быстро находить символы, упорядоченные по числу их повторений. Алгоритмические подробности я опущу, отсылая читателя по ссылке выше (пардон за тавтологию). Собственно, на этом всё бы и закончилось, и не было бы никакой статьи, потому что кодирование я реализовывал только в качестве тренировки в алгоритмике. Но по ходу работы я заметил, как же быстро компилируется программа на C по сравнению с подобного размера исходниками на C++. И упомянул об этом коллеге по работе. Высказав предположение, что компиляция на C++ включает, наверное, ещё множество способов оптимизации. Так что подобно написанный код на C++, наверное, должен быть быстрее — там же будет работать магия самых-самых гуру в области написания оптимизирующих компиляторов. Ухмыльнувшись, коллега ответил: «Проверь».
И тогда я переписал кодирование Хаффмана на C++. Для чистоты эксперимента я не менял основополагающих принципов, к примеру, не вставлял пользовательский распределитель памяти. Это можно сделать и в C (более «кастомно») и в C++ (более «нативно»). В чём же тут тогда «C++-ность»?
Очередь с приоритетом
Первое, что логично выразить через шаблоны в C++, это очередь с приоритетом. На C она представлена в виде структуры, главный элемент которой — указатель на массив указателей на узлы с данными:
struct priority_queue < // A number of active (carrying data) nodes currently in the queue unsigned int size; // A total number of nodes in "nodes" array unsigned int capacity; // An array of pointers to nodes struct priority_queue_node** nodes; >;
Узел с данными может быть любым типом, но его первым элементом должна быть следующая структура:
struct priority_queue_node < unsigned int weight; >;
Так как очередь не занимается управлением памятью под сами узлы, ей незачем знать, из чего реально состоит узел. Всё, что требуется для работы, это получить его вес: ((struct priority_queue_node*) node_ptr)→weight. Добавление узла в очередь с учётом возможного перевыделения памяти выглядит несколько громоздко:
int priority_queue_push(struct priority_queue* queue, struct priority_queue_node* node) < if (queue->size >= queue->capacity) < int new_capacity = queue->capacity * 2; if (new_capacity == 0) new_capacity = 1; struct priority_queue_node** new_nodes = (struct priority_queue_node**) malloc(sizeof(struct priority_queue_node*) * new_capacity); if (! new_nodes) < return 0; >memcpy(new_nodes, queue->nodes, sizeof(struct priority_queue_node*) * queue->size); if (queue->capacity) free(queue->nodes); queue->nodes = new_nodes; queue->capacity = new_capacity; > queue->nodes[queue->size++] = node; heapify(queue); return 1; >
Создание очереди и её удаление с обработкой всех ошибок — тоже много строчек кода по сравнению с C++ версией, что ожидаемо. Собственно, версия очереди на C++ выглядит так (внимание — на представление данных):
template class priority_queue < struct node < unsigned int m_weight; T m_data; >; using node_ptr = std::unique_ptr; std::size_t m_capacity; std::size_t m_size; std::unique_ptr m_nodes; void heapify() noexcept; void increase_capacity(); public: explicit priority_queue(std::size_t capacity = 16) ; // … >;
Налицо такое же расположение элементов узла в памяти, как и в C версии — сначала идёт вес, затем полезная нагрузка узла, которая, как будет показано ниже, состоит из целочисленного скаляра — высоты дерева, содержащегося в этом узле, и указателя на корень этого дерева. Сами узлы в этой очереди приоритетов хранятся также — указатель m_nodes указывает на массив указателей на узлы.
Для сравнения — положить новый элемент в очередь теперь выглядит более лаконично и типобезопасно (перевыделение памяти — в отдельном методе increase_capacity, что не меняет сути):
template push(unsigned int weight, U&& obj) < if (m_size >= m_capacity) increase_capacity(); m_nodes[m_size++].reset(new node((obj)>)); heapify(); > void increase_capacity() < const auto new_capacity = m_capacity ? m_capacity * 2 : 1; std::unique_ptrnew_nodes(new node_ptr[new_capacity]); for (auto src = m_nodes.get(), dest = new_nodes.get(); src != m_nodes.get() + m_size; ++src, ++dest) *dest = std::move(*src); m_nodes = std::move(new_nodes); m_capacity = new_capacity; >
Дерево символов (дерево кодирования)
В очередь впоследствии будут вставляться части дерева символов, которые затем будут объединяться в соответствии с числом повторений каждого символа так, чтобы в конце получить дерево кодирования. На C это дерево представляется простейшими структурами с сырыми указателями, базовая из них содержит идентификатор конечного типа:
#define NODE_TYPE_TERM 1 #define NODE_TYPE_NODE 2 struct char_node_base < int type; >; struct char_node_terminal < struct char_node_base base; char c; >; struct char_node < struct char_node_base base; struct char_node_base* left; struct char_node_base* right; >;
А чтобы положить корень такого дерева в очередь с приоритетом, определена структура с, требуемым очередью, членом — хранителем веса узла:
struct char_node_root < struct priority_queue_node pq_node; int height; struct char_node_base* node; >;
На C++ всё это выражается несколько элегантнее:
struct char_node_base < virtual ~char_node_base() = default; >; using char_node_ptr = std::unique_ptr; struct char_node_terminal : char_node_base < const unsigned char m_c; char_node_terminal(char c) noexcept : m_c(c) <>>; struct char_node : char_node_base < char_node_ptr m_left; char_node_ptr m_right; >; struct nodes_root < int m_height; char_node_ptr m_node; >;
Здесь видно ключевое преимущество C++ — для корректного удаления этого дерева не нужно делать ничего. Просто удалить корень, авто указатели всё сделают сами. В C же для этой задачи написано изрядное количество кода.
Заполнение очереди и построение дерева
Этот шаг вообще не отличается для C и C++ реализации. Сначала просчитывается число повторений каждого символа во входном блоке данных, и заполняется табличка из 256 байт. Потом в очередь с приоритетом кладутся микро-деревья, состоящие только из одного терминального узла. Далее узлы объединяются путём последовательного извлечения из очереди пары, ближайшей к её вершине, и вставкой туда промежуточного узла, содержащего извлечённые прежде.
На C (здесь — без проверки на ошибки) это выглядит следующим образом:
static struct priority_queue* build_priority_queue( char* buffer, unsigned int size) < unsigned char table[256]; memset(table, 0, sizeof(table)); for (unsigned int i = 0; i < size; ++i) if (table[(unsigned char)buffer[i]] != 255) ++table[(unsigned char)buffer[i]]; struct priority_queue* queue = priority_queue_create(16); for (unsigned short i = 0; i < 256; ++i) < if (table[i]) < struct char_node_root* node = (struct char_node_root*) malloc(sizeof(struct char_node_root)); struct char_node_terminal* term = (struct char_node_terminal*) malloc(sizeof(struct char_node_terminal)); term->base.type = NODE_TYPE_TERM; term->c = (char)i; node->node = (struct char_node_base*) term; node->height = 0; node->pq_node.weight = table[i]; priority_queue_push(queue, (struct priority_queue_node*) node); > > return queue; > static struct char_node_root* queue_to_tree(struct priority_queue* queue) < while (priority_queue_size(queue) >1) < struct char_node_root* node1 = (struct char_node_root*) priority_queue_pop(queue); struct char_node_root* node2 = (struct char_node_root*) priority_queue_pop(queue); struct char_node_base* int_node1 = node1->node; struct char_node_base* int_node2 = node2->node; struct char_node* join_node = (struct char_node*) malloc(sizeof(struct char_node)); join_node->base.type = NODE_TYPE_NODE; join_node->left = int_node1; join_node->right = int_node2; int new_weight = node1->pq_node.weight; if (new_weight + node2->pq_node.weight pq_node.weight; else new_weight = 65535; node1->pq_node.weight = new_weight; if (node1->height > node2->height) ++node1->height; else node1->height = node2->height + 1; free(node2); node1->node = (struct char_node_base*) join_node; priority_queue_push(queue, (struct priority_queue_node*) node1); > return (struct char_node_root*) priority_queue_pop(queue); >
На C++ — ещё короче и красивее при том, что любые ошибки выделения памяти будут обработаны корректно благодаря исключениям и применению авто указателей:
void fill_priority_queue( const unsigned char* buffer, std::size_t buffer_size, queue_t& queue) < unsigned char counts_table[256]<>; for (auto ptr = buffer; ptr != buffer + buffer_size; ++ptr) if (counts_table[*ptr] != 255) ++counts_table[*ptr]; for (unsigned short i = 0; i != 256; ++i) if (counts_table[i]) queue.push(counts_table[i], nodes_root ); > void queue_to_tree(queue_t& queue) < while (queue.size() >1) < auto old_root1_node = std::move(queue.top()); const auto old_root1_weight = queue.top_weight(); queue.pop(); auto old_root2_node = std::move(queue.top()); const auto old_root2_weight = queue.top_weight(); queue.pop(); auto joined_node = std::unique_ptr(new char_node); joined_node->m_left = std::move(old_root1_node.m_node); joined_node->m_right = std::move(old_root2_node.m_node); const auto new_weight = std::min(old_root1_weight + old_root2_weight, 65535U); const auto new_height = std::max(old_root1_node.m_height, old_root2_node.m_height) + 1; queue.push(new_weight, nodes_root ); > >
Таблица кодирования
Следующим этапом из дерева кодирования нужно построить таблицу, где каждому символу в соответствие поставлена последовательность бит, отражающих прохождение по, только что построенному, дереву кодирования до этого символа. Код для обхода дерева и «набивания» последовательности бит в обоих версиях практически идентичен. Наибольший интерес же представляет то, как хранить эту последовательность бит и оперировать ею. Ведь после построения этой таблицы начнётся то, ради чего всё затевалось. Проходя по входному буферу для каждого, найденного там символа, будет браться его битовая последовательность и добавляться к результирующей выходной последовательности.
В C версии последовательность бит представляется тривиальной структурой, состоящей из указателя на массив байт и счётчика реально заполненных бит. Каких-то специальных операций для работы с ней нет. На этапе построения таблицы кодирования для каждого символа заводится такая структурка, куда копируется, найденная для текущего символа, последовательность бит. Таким образом, таблица кодирования — это просто массив этих структур для каждого символа.
struct bits_line < unsigned char bits_count; unsigned char* bits; >; static int build_encoding_map_node(struct char_node_base* node, struct bits_line* bits_table, unsigned char* bits_pattern, int bits_count) < if (node->type == NODE_TYPE_TERM) < unsigned char index = (unsigned char)((struct char_node_terminal*)node)->c; bits_table[index].bits_count = bits_count; bits_table[index].bits = (unsigned char*) malloc(bytes_count_from_bits(bits_count + 1)); if (! bits_table[index].bits) return 0; memcpy(bits_table[index].bits, bits_pattern, bytes_count_from_bits(bits_count)); return 1; > static const unsigned char bit_mask[] = ; bits_pattern[bits_count >> 3] &= ~bit_mask[bits_count & 7]; if (! build_encoding_map_node(((struct char_node*)node)->left, bits_table, bits_pattern, bits_count + 1)) return 0; bits_pattern[bits_count >> 3] |= bit_mask[bits_count & 7]; if (! build_encoding_map_node(((struct char_node*)node)->right, bits_table, bits_pattern, bits_count + 1)) return 0; return 1; >
В C++ версии битовый массив удобнее представить полноценным классом, который будет не просто управлять ресурсами, но и поддерживать, нужную далее, операцию добавления другой битовой последовательности.
using unique_bytes_ptr = std::unique_ptr; class bit_ostream < std::size_t m_capacity; unsigned long m_bits_count = 0; unique_bytes_ptr m_data; public: explicit bit_ostream(std::size_t initial_capacity = 0) noexcept : m_capacity(initial_capacity) < >bit_ostream& push(const unsigned char* bits, unsigned long const bits_count) < if (bits_count == 0) return *this; const auto new_bits_count = m_bits_count + bits_count; if (covered_bytes(new_bits_count) + 1 >m_capacity || m_bits_count == 0) < decltype(m_capacity) new_capacity = m_capacity * 2; const auto cov_bytes = static_cast(covered_bytes(new_bits_count) + 1); if (new_capacity < cov_bytes) new_capacity = cov_bytes; unique_bytes_ptr new_data(new unsigned char[new_capacity]); std::memcpy(new_data.get(), m_data.get(), covered_bytes(m_bits_count)); m_capacity = new_capacity; m_data = std::move(new_data); >unsigned char* curr = m_data.get() + (m_bits_count >> 3); if ((m_bits_count & 7) == 0) < // All it's simple when current output data size is integer number of bytes std::memcpy(curr, bits, covered_bytes(bits_count)); >else < const unsigned char shift = m_bits_count & 7; for (auto bytes_count = covered_bytes(bits_count); bytes_count >0; ++curr, ++bits, --bytes_count) < unsigned short val = static_cast(*bits) << shift; val |= static_cast(*curr & g_bits_fill_mask[shift]); *curr = static_cast(val & 0xff); *(curr + 1) = static_cast(val >> 8); > > m_bits_count += bits_count; assert(covered_bytes(m_bits_count) bit_ostream& push(const bit_ostream& other) < return push(other.data(), other.bits_count()); >bit_ostream& clear_tail() noexcept < if (m_bits_count & 7) m_data.get()[m_bits_count >> 3] &= g_bits_fill_mask[m_bits_count & 7]; return *this; > unsigned long bits_count() const noexcept < return m_bits_count; >bool empty() const noexcept < return ! m_bits_count; >unsigned char* data() noexcept < return m_data.get(); >const unsigned char* data() const noexcept < return m_data.get(); >>; template constexpr inline std::size_t covered_bytes(T bits_count) noexcept < return (bits_count >> 3) + (bits_count & 7 ? 1 : 0); >
Соберём всё воедино
Итак, все составляющие процедуры кодирования разобраны выше. Повторим ещё раз вкратце последовательность шагов. Здесь важно упомянуть, что для дальнейших измерений с целью сравнения производительности эта последовательность разбита на этапы. Измерения по этапам в самой программе я делал самым быстрым, известным мне, способом — чтением счётчика циклов CPU с помощью инструкции rdtsc.
- Запомнить точку во времени ts1 с помощью rdtsc.
- Заполнить очередь с приоритетом символами по числу их повторений.
- Построить дерево символов с помощью этой очереди. Удалить саму очередь.
- Вычислить число циклов t1, прошедшее с момента ts1, и запомнить следующую точку ts2.
- Построить таблицу кодирования, обходя дерево кодирования. Уничтожить дерево кодирования.
- Вычислить число циклов t2, прошедшее с момента ts2, и запомнить следующую точку ts3.
- Осуществить собственно кодирование входного потока, заменяя каждый входной символ последовательностью бит из таблицы кодирования.
- Вычислить число циклов t3, прошедшее с момента ts3.
Замеры и оптимизация
Нетерпеливый читатель, проглядевший столько кода выше, уже ёрзает на стуле, задаваясь вопросом: «Ну так что там получилось?». Попробуем запустить обе версии, скомпилированного компилятором gcc-5.4.0 с уровнем оптимизации «O3», упаковщика на файле размером около 31 Мб. Нужно отметить, что для кодирования можно выбирать разные размеры блока данных из входного файла. По умолчанию это 64 Кб. То есть, осуществляется кодирование 31 Мб / 64 Кб блоков, а все показатели времени суммируются.
> build-c/pack-c -m3 ../sample-1.dat data-c.dat File packing is done. Read 31962362 bytes, written 32031809 bytes. Total ticks = 1053432 (0.754 seconds), t1 = 209957, t2 = 31023, t3 = 811377. > build-cpp/pack-cpp -m3 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 32031809 bytes. Total ticks = 1182005 (0.846 seconds), t1 = 228527, t2 = 52680, t3 = 894081
На опцию «-m3» можно не обращать внимание, это просто переключатель, означающий тестовый режим.
Ну что-ж, как-то не очень весело. То есть, C++ не дался бесплатно, провал по производительности порядка 12%. Все три этапа выполняются дольше, чем в C версии. А если размер блока выбрать поменьше, скажем, 1 Кб?
> build-c/pack-c -m3 -b1024 ../sample-1.dat data-c.dat File packing is done. Read 31962362 bytes, written 31160081 bytes. Total ticks = 9397894 (6.731 seconds), t1 = 5320910, t2 = 1943422, t3 = 2094688. > build-cpp/pack-cpp -m3 -b1024 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 31160081 bytes. Total ticks = 11586220 (8.3 seconds), t1 = 6399593, t2 = 3125111, t3 = 1663035
Понятное дело, просело всё, потому что теперь нужно гораздо чаще перестраивать дерево кодирования. Но C опять вырвался вперёд — аж на 23%!
Оптимизация «на глазок»
Что не так с C++ реализацией? Вроде один компилятор, один и тот же оптимизатор там. По счётчикам циклов выше видно, что самый большой вклад дают шаги, где начинается манипуляция с битами. Класс bit_ostream получился хороший. Но при наполнении таблицы кодирования так ли хорош его, чрезмерно нагруженный для составления таблицы, метод push? Ведь положить массив бит в изначально пустой объект должно быть куда проще, чем весь код в том методе. Да и таблица кодирования, составленная из 256 сущностей этого класса, занимает гораздо больше места, чем 256 структур bits_line из C версии. Попробуем сделать вариант этого класса для таблицы кодирования.
class small_bit_ostream < unique_bytes_ptr m_data; unsigned short m_bits_count = 0; public: small_bit_ostream& push(const unsigned char* bits, const unsigned short bits_count) < const auto cov_bytes ; m_data.reset(new unsigned char[cov_bytes]); std::memcpy(m_data.get(), bits, cov_bytes); m_bits_count = bits_count; return *this; > unsigned long bits_count() const noexcept < return m_bits_count; >bool empty() const noexcept < return ! m_bits_count; >unsigned char* data() noexcept < return m_data.get(); >const unsigned char* data() const noexcept < return m_data.get(); >>;
Просто. Красиво. Ничего лишнего. Даёт ли это хоть что-то? (Не тронутую C версию не привожу тут.)
> build-cpp/pack-cpp -m3 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 32031809 bytes. Total ticks = 1173692 (0.84 seconds), t1 = 229942, t2 = 46677, t3 = 890323 > build-cpp/pack-cpp -m3 -b1024 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 31160081 bytes. Total ticks = 11198578 (8.02 seconds), t1 = 6404650, t2 = 2752852, t3 = 1641317
Ну что, для большого блока улучшение — на уровне погрешности. Для маленького блока улучшение чуть заметнее — теперь C++ хуже всего на 19%. Видно по показателю t2, что заполнение таблицы стало лучше работать.
Профилирование
Начнём с проверки, как поживают кэши CPU. Запустим обе версии ПО под valgrind’ом с инструментарием «cachegrind». Вот краткий вывод для C версии.
==2794== I refs: 2,313,382,347 ==2794== I1 misses: 14,482 ==2794== LLi misses: 1,492 ==2794== I1 miss rate: 0.00% ==2794== LLi miss rate: 0.00% ==2794== ==2794== D refs: 601,604,444 (472,330,278 rd + 129,274,166 wr) ==2794== D1 misses: 3,966,884 ( 2,279,553 rd + 1,687,331 wr) ==2794== LLd misses: 7,030 ( 3,034 rd + 3,996 wr) ==2794== D1 miss rate: 0.7% ( 0.5% + 1.3% ) ==2794== LLd miss rate: 0.0% ( 0.0% + 0.0% ) ==2794== ==2794== LL refs: 3,981,366 ( 2,294,035 rd + 1,687,331 wr) ==2794== LL misses: 8,522 ( 4,526 rd + 3,996 wr) ==2794== LL miss rate: 0.0% ( 0.0% + 0.0% ) ==2794== ==2794== Branches: 299,244,261 (298,085,895 cond + 1,158,366 ind) ==2794== Mispredicts: 8,779,093 ( 8,778,920 cond + 173 ind) ==2794== Mispred rate: 2.9% ( 2.9% + 0.0% )
А вот и вывод для C++ версии с теми же параметрами:
==2994== I refs: 2,464,681,889 ==2994== I1 misses: 2,032 ==2994== LLi misses: 1,888 ==2994== I1 miss rate: 0.00% ==2994== LLi miss rate: 0.00% ==2994== ==2994== D refs: 633,267,329 (491,590,332 rd + 141,676,997 wr) ==2994== D1 misses: 3,992,071 ( 2,298,593 rd + 1,693,478 wr) ==2994== LLd misses: 8,292 ( 3,173 rd + 5,119 wr) ==2994== D1 miss rate: 0.6% ( 0.5% + 1.2% ) ==2994== LLd miss rate: 0.0% ( 0.0% + 0.0% ) ==2994== ==2994== LL refs: 3,994,103 ( 2,300,625 rd + 1,693,478 wr) ==2994== LL misses: 10,180 ( 5,061 rd + 5,119 wr) ==2994== LL miss rate: 0.0% ( 0.0% + 0.0% ) ==2994== ==2994== Branches: 348,146,710 (346,241,481 cond + 1,905,229 ind) ==2994== Mispredicts: 6,977,260 ( 6,792,066 cond + 185,194 ind) ==2994== Mispred rate: 2.0% ( 2.0% + 9.7% )
Можно заметить, что по попаданию в кэш и по данным и по инструкциям C++ не хуже, а иногда даже и лучше, чем C код. Предсказание переходов вообще лучше работает. А почему же он проваливается слегка? Очевидно, что в нём просто выполняется больше инструкцкий — 2 464 млн. против 2 313. Что и даёт примерно ту разницу в производительности, что была заметна при использовании больших блоков.
При анализе инструментарием «callgrind» видно, что много инструкций тратится на работу с кучей — malloc и free. Но помимо этого в C++ версии встречаются ещё и значительные, с точки зрения инструкций, упоминания операторов new и delete. А всегда ли они нужны? Те самые операции с битовыми массивами, что отдельно упоминались выше, реализованы с использованием авто указателя unique_ptr, для которого память выделяется с помощью new[]. Вспомним, что данный оператор внутри обращается к C-шному malloc, а затем инициализирует каждый объект в созданном массиве. То есть в нашем случае заполняет массив нулями. А зачем это программе? Класс bit_ostream сразу после получения массива заполняет его битами, дописывая их в конец. И хранит счётчик записанных бит. Ему вовсе не нужно, чтобы массив байт был предварительно очищен. Попробуем написать простейший адаптер для управления памятью через malloc / free, но с использованием unique_ptr, чтобы так же удобно не думать о её очистке.
struct free_deleter < void operator()(void* p) const noexcept < std::free(p); >>; template inline T* allocate_with_malloc(std::size_t size) < T* res = static_cast(std::malloc(sizeof(T) * size)); if (! res) throw std::bad_alloc(); return res; > template using unique_malloc_array_ptr = std::unique_ptr; template inline unique_malloc_array_ptr unique_allocate_with_malloc(std::size_t size) < return unique_malloc_array_ptr(allocate_with_malloc(size)); > // Typedefs for byte arrays using unique_bytes_ptr = unique_malloc_array_ptr; inline unique_bytes_ptr allocate_bytes(std::size_t size) < return unique_bytes_ptr(unique_allocate_with_malloc(size)); >
Проверим предположение на том же файле с кодированием большими и малыми блоками (как и прежде, C версия не менялась, так что её не привожу).
> build-cpp/pack-cpp -m3 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 32031809 bytes. Total ticks = 1042665 (0.746 seconds), t1 = 250480, t2 = 45393, t3 = 740163 > build-cpp/pack-cpp -m3 -b1024 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 31160081 bytes. Total ticks = 11068384 (7.93 seconds), t1 = 6488100, t2 = 2694562, t3 = 1501027
Ну что-ж, на больших блоках C++ реализация обогнала C! На малых всё пока что хуже, хотя лучше, чем в предыдущем эксперименте. Количество инструкций на всю программу — 2 430 млн. вместо 2 464. Количество обращений к данным тоже сократилось с 633 млн. до 536. Понятно, что на малых блоках новая реализация практически осталась как была — там же в основном играет роль построение дерева кодирования, а его код не менялся.
Ещё пару капелек
Обратим внимание на очередь с приоритетом, которую так красиво и лаконично удалось реализовать на C++. Она, как и всё остальное, использует авто указатели для управления памятью. Есть один главный указатель m_nodes, который указывает на массив указателей на узлы. В ходе выполнения любой изменяющей операции содержимое конечных указателей переставляется, как правило, выражением ptr1 = std::move(ptr2). Что тут «зарыто»? Указатель ptr1 должен проверить, что он ни на что не указывает, в противном случае удалить ресурс. Указатель ptr2 должен обнулиться после того, как ресурс у него заберут. Да, это малое количество инструкций, тут почти не о чем разговаривать. Но! Во всех операциях с очередью строго известно, когда и что на что указывает. Поэтому таких проверок и обнулений делать не надо. А копирование сырых указателей занимает одну (!) инструкцию. Давайте заменим конечные указатели в очереди с приоритетом на сырые и прогоним тесты ещё раз.
> build-cpp/pack-cpp -m3 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 32031809 bytes. Total ticks = 1008990 (0.722 seconds), t1 = 221001, t2 = 44870, t3 = 736557 > build-cpp/pack-cpp -m3 -b1024 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 31160081 bytes. Total ticks = 10683068 (7.65 seconds), t1 = 6101534, t2 = 2689178, t3 = 1505929
Ну что-ж, на больших блоках C++ быстрее на 4,3%, на малых — медленнее всего на 13,6%. Инструкций теперь 2 413 млн., обращений к данным — 531 млн.
Заключение
К каким мыслям я пришёл по ходу такого сравнительного анализа на примере задачки по кодированию Хаффмана?
- Сначала обе версии программы я реализовал «в лоб», не особенно задумываясь о каких-то специфических оптимизациях. Но так получилось, что C версия сразу получилась быстрее, а C++ я «дотягивал» до неё, изучал, профилировал и т. п. В результате я получил в среднем такое же быстрое решение. (Я думаю, что и этап построения дерева кодирования можно «допилить», чтобы он не «провисал» по скорости.) Но я хотел отметить то, что сам процесс написания программы на C «вёл меня» по пути создания быстрого кода, а в случае C++ пришлось заниматься дальнейшим анализом.
- Тем, кто хорошо знает C++, писать на нём гораздо удобнее, чем на C. Программы получаются лаконичнее и они лишены множества потенциальных ошибок, которые можно сделать на C. По ходу написания и отладки C программы я столкнулся с множеством (сравнивая с C++) случаев некорректного использования указателей, обращения к очищенной памяти, неверного преобразования типов. В хорошо типизированной C++ программе по максимуму действует правило — компилируется, значит корректна. Удаление сложных структур вместе с механизмом исключений — вообще сила, потому что здесь не нужно ни строчки пользовательского кода (разве что вывести сообщение об ошибке), а программа корректно обрабатывает такие ситуации «из коробки».
- Заметить, что что-то работает медленно в ходе профилирования очень сложно, потому что это субъективная оценка. У меня было две реализации, и я мог сравнить эквивалентные шаги. Но в реальности будет только одна реализация, потому что в начале выбрали для неё язык «XYZ». Как понять, быстро она работает, или медленно? Сравнить то не с чем. Если бы я написал только C++ реализацию и в конце замерил, что она обрабатывает 31 Мб за 0.85 секунды, я бы сразу считал, что это эталон скорости!
- Что же касается лично моих предпочтений при написании программ в будущем, то здесь подход следующий. Если мне нужно написать «молотилку», действия которой в основном будут состоять из миллионов похожих действий, то лучше писать её в C стиле, чтобы увидеть/реализовать самостоятельно все, пусть даже самые незначительные, шаги. Ведь каждая лишняя инструкция тут на миллионах повторений даст просадку. Если же я пишу сложную управляющую логику с очень разнообразными действиями, не сводящимися к «повторить вычисления A, B, C миллион раз», то лучше взять на вооружение всю мощь типизированного C++. Потому что конечное время работы такой программы уже не будет сводиться к вопросу «а за сколько времени она обработает терабайт данных», управляющая логика будет зависеть от множества внешних факторов и сценариев использования. И говорить о точных скоростных характеристиках не придётся. А вот корректность такой логики «из коробки» будет в сто крат ценнее, потому что никому не захочется вычищать из неё баги в C версии до скончания веков. И иметь в конце концов «жёсткую» версию кода, которую невозможно изменить, адаптировать под новые нужды, потому что, где бы ни тронул, всё посыпется. И начинай отладку сначала.
Update 1. По просьбам в комментариях я проверил работу упаковщиков, собранных компилятором clang-3.8.0. Результаты таковы.
> build-c/pack-c -m3 ../sample-1.dat data-c.dat File packing is done. Read 31962362 bytes, written 32031809 bytes. Total ticks = 1139373 (0.816 seconds), t1 = 206305, t2 = 29559, t3 = 902493.
Неоптимизированная C++ версия:
> build-cpp/pack-cpp -m3 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 32031809 bytes. Total ticks = 1417576 (1.01 seconds), t1 = 223441, t2 = 53057, t3 = 1134400
Оптимизированная C++ версия:
> build-cpp/pack-cpp -m3 ../sample-1.dat data-cpp.dat File packing is done. Read 31962362 bytes, written 32031809 bytes. Total ticks = 1174028 (0.84 seconds), t1 = 215059, t2 = 59738, t3 = 892821
Относительный расклад сил не меняется, а в абсолютном плане clang генерирует код чуть-чуть медленнее, чем gcc.
C++ vs C#: Сравнение и противопоставление
C++ — это язык программирования среднего уровня, более быстрый и близкий к машинному коду. C# — это язык программирования высокого уровня, который легче изучить. И C++, и C# являются объектно-ориентированными языками программирования общего назначения.
Хостинг и Vps для вашего сайта от GoHost.kz
Как программист, вам необходимо освоить множество языков, чтобы работать над разными типами проектов. Погружаясь глубже в мир кодирования, вы, вероятно, столкнетесь с двумя широко используемыми языками: C++ и C#. Вы можете задаться вопросом, какой из них лучше и на какой я должен сосредоточиться? В этом руководстве мы рассмотрим вопрос C# и C++.
Семейная история программирования на C

С
C был разработан в 1970-х годах для работы с операционной системой UNIX, которая в то время только развивалась. C — это язык программирования гораздо более низкого уровня, чем основанные на нем языки, такие как C++ и C#. Это означает, что его можно использовать почти так же эффективно, как ассемблерный код, формирующий основные строительные блоки компьютерных инструкций. Однако, как и в любом низкоуровневом языке, написание чего-либо сложного на C может быть немного утомительным, и этот язык не так прост для понимания. C до сих пор используется в различных приложениях. Встроенные системы, такие как программное обеспечение, которое находится в любом промышленном оборудовании или бытовой технике, часто используют C, поскольку он не занимает много места. Он также используется для сценариев процессов в веб-приложениях на стороне сервера или везде, где небольшие, но быстрые программы должны работать в фоновом режиме. Практически на любом компьютере, который вы используете, где-то под капотом работает C.
С++
C++ был создан датским аспирантом по имени Бьерн Страуструп в 1979 году. Он хотел расширить возможности языка. Само название отражает то, как C++ выходит за рамки C. Суффикс ++ указывает на увеличение ценности по сравнению с оригиналом. Он был разработан как расширение C или C с классами. Это конкретно означало C с объектно-ориентированными возможностями.
С#
C# был разработан Microsoft в 2002 году. Хотя технически он основан на языке под названием .NET, он во многом обязан C. Он был разработан как конкурент Java и имеет некоторое сходство с этим языком. На самом деле его создание произошло потому, что Sun, владельцы Java, не хотели, чтобы Microsoft вносила изменения в их язык, поэтому Microsoft решила создать свою собственную альтернативу. C++ удовлетворил потребность в объектно-ориентированном программировании внутри C. C# был построен на успехе этого языка и Java, другого популярного объектно-ориентированного языка.
Важные особенности C++
- Машинно-независимый . Будучи машинно-независимым языком, вы можете один раз написать программу на C++, а затем запустить ее в любой операционной системе. Однако он не зависит от платформы, а это означает, что он создает разные файлы .exe на каждой платформе.
- Объектно-ориентированный . C++ является объектно-ориентированным, что облегчает чтение, запись и устранение неполадок, а также упрощает внесение изменений без необходимости изменения всей структуры кода.
- На основе компилятора . Код, написанный на C++, компилируется, после чего он транслируется непосредственно в инструкции, которые машина может интерпретировать напрямую.
- Нет автоматического сбора мусора. C++ не имеет автоматической сборки мусора, а это означает, что вам придется вручную выделять и освобождать память в своих программах.
- Промежуточный уровень. Он считается промежуточным языком, потому что упрощает код и запускает его независимо от машины, но он также зависит от аппаратного обеспечения или языков машинного программирования.
Важные особенности C#
Некоторые из основных различий между C# и C++ включают способ компиляции и управление использованием памяти. Продолжайте читать, чтобы узнать о некоторых основных функциях C#, которые помогут углубить ваше понимание этого языка.
- В основном используется для Windows . C# был разработан как конкурент Java для Windows, поэтому он редко используется для других операционных систем.
- Объектно-ориентированный . C# также является объектно-ориентированным языком, в котором как данные, так и функции, работающие с данными, сгруппированы вместе как объект. C# считается компонентно-ориентированным языком программирования. Это означает, что C# имеет особый уклон в сторону повторного использования старых компонентов.
- Компилируется в CLR . Код C# компилируется в Common Machine Runtime или CLR, который интерпретируется Just In Time (JIT) в ASP.NET.
- Автоматическое управление памятью . C# автоматически обрабатывает управление памятью с помощью сборщика мусора.
- Язык высокого уровня . C# использует синтаксис, напоминающий человеческий язык, и имеет высокий уровень абстракции от машинного кода.
C# и C++: ключевые сходства
Одно из ключевых сходств между C++ и C# заключается в том, что оба языка являются производными от C. Это означает, что их синтаксис и использование символов уходят корнями в C. Кроме того, оба языка являются объектно-ориентированными и поддерживают полиморфизм. Другое важное сходство заключается в том, что оба они являются компилируемыми языками.
C# vs C++: ключевые отличия
Одно из ключевых отличий заключается в том, что в C++ нет автоматической сборки мусора, а это означает, что вам придется вручную выделять и освобождать память в своих программах. C# автоматически обрабатывает управление памятью с помощью сборщика мусора. C++ не предупреждает пользователей о каких-либо ошибках перед компиляцией при соблюдении синтаксиса. C# предупреждает пользователей об ошибках компилятора.
Сравнение С++ и С#

Хотя у них общий предок, C++ и C# стали очень разными языками. Как указывалось ранее, C# — это язык более высокого уровня по сравнению с C++ или C. Продолжайте читать, чтобы узнать больше об их приложениях, производительности и причинах популярности каждого языка.
Популярность
По данным Statista, и C++, и C# по-прежнему входят в десятку самых популярных языков программирования среди разработчиков в 2023 году. Оба языка завоевали солидную репутацию в сообществе разработчиков. C++ по-прежнему популярен для разработки игр благодаря своей высокой производительности, в то время как C# по-прежнему широко используется для веб-приложений и настольных приложений.
Производительность и скорость
При сравнении C# и C++ по производительности и скорости важно помнить, что выбор будет зависеть от типа проекта, над которым вы работаете. В общих чертах, код C++ будет выполняться быстрее, чем код C#. Это делает его лучшим выбором для приложений, где скорость является неотъемлемой частью взаимодействия с пользователем. Однако с точки зрения скорости разработки C#, как правило, быстрее.
Языки высокого уровня, такие как C#, предназначены для сокращения времени написания кода за счет абстрагирования множества скрытых процессов. Так что, если вам нужно быстрее обрабатывать числа в реальном времени, C++ — это то, что вам нужно. Если вам нужно что-то быстро собрать, используйте C#.
Разработка игр
Что касается вопроса о C++ и C# для разработки игр, вы должны знать, что оба языка являются жизнеспособными вариантами, причем C++ берет верх. Популярные игровые движки, такие как Unreal или GameMaker, используют C++ благодаря его лучшей производительности и более эффективному управлению памятью. C# — это вариант для разработки игр, если вы хотите писать код специально для экосистемы .NET или для механизмов разработки, таких как Unity, Wave и Stride.
C# vs C++: что выбрать?
Теперь, когда вы знаете разницу между C# и C++, что следует изучить в первую очередь? C# обычно считается более простым языком для изучения по сравнению с C++ из-за его статуса более высокого уровня и удобства использования платформы .NET.
Оба языка стоит изучать для возможности трудоустройства в области разработки программного обеспечения или информатики. Если вы хотите заняться более фундаментальной обработкой чисел и низкоуровневыми вычислениями, C++ будет хорошим местом для начала. Если вы хотите сразу же создавать приложения или видеоигры, обязательно начните с C#.
Часто задаваемые вопросы о С# и С++
C# легче выучить, чем C++?
Да, C# обычно считается более легким для изучения, чем C++. В то время как оба могут быть сложными для абсолютных новичков, C++ в целом является более сложным языком, что приводит к более крутой кривой обучения.
В чем разница между C++ и Visual C++?
Разница между C++ и Visual C++ заключается в том, что первый — это язык программирования, а Visual C++ — интегрированная среда разработки или IDE. Visual C++ — это компилятор для C и C++.
Каковы некоторые различия между C++ и Java?
Основное различие между C++ и Java заключается в том, что C++ является компилируемым языком, а Java является одновременно компилируемым и интерпретируемым языком. Другое важное отличие заключается в том, что Java не зависит от платформы, чего нельзя сказать о C++.
С# лучше, чем С++?
C# лучше C++ с точки зрения простоты использования и скорости создания кода. Однако C++ лучше с точки зрения производительности, что делает его лучшим вариантом для разработки приложений, где скорость является важным фактором.
C vs. C++: Полное Сравнение Языков Программирования
Противостояние C vs С++. Что лучше выбрать C или C++? Всё о преимуществах и недостатках этих языков. Детальное сравнение C vs C++.
Обновлено: January 01, 2024

Даже по сей день, несмотря на наличие множества простых для освоения языков программирования, многие традиционные образовательные учреждения до сих пор обучают таким языкам как C или C++ в качестве обязательных курсов. Конечно, это имеет свои недостатки (даже не затрагивая сам спор C vs C++), но многие опытные разработчики считают, что это самый оптимальный способ обучения программированию. Аргументом является то, что даже если эти языки сложнее, чем тот же Python или Java, они помогут новичкам сформировать прочный фундамент и предоставить более детальное понимание процессов программирования.
Но даже если популярность двух вышеупомянутых языков программирования очевидна, многие люди до сих пор не знают отличие С от С++. Что же, в этой статье мы проведём детальное сравнение C vs C++. Мы начнём с общего обзора программирования, а затем расскажем про каждый из языков в отдельности. После этого мы определим критерии для анализа, а уже затем проведём сравнение C vs C++.
Введение
Для некоторых программирование говорит само за себя, но не каждый уверен в том, что хочет начать его изучать. Зачем вообще стараться осваивать программирование, когда в мире есть другие, более простые и менее времязатратные специализации? Подобного рода мысли могут посеять сомнения у любого человека, особенно, если вы только начинаете свой путь в этой сфере. Перед тем как начать наше сравнение C vs C++, давайте поговорим о самых распространённых причинах, по которым люди начинают изучать программирование.
Для начала, востребованность специалистов. Ни для кого не секрет, что сфера информационных технологий в общем, предлагает множество вариантов для трудоустройства. Причины этого очень просты. С постоянным развитием индустрии технологий и появлением новых технологий почти каждый день, вроде разработки AI, науки о данных, программирование становится всё более популярным. Поэтому, благодаря этой популярности, увеличивается и спрос на надёжных и профессиональных специалистов в этой сфере. Хватит лишь беглого взгляда на рынок труда, чтобы понять, подобный тренд на специальности программирования будет держаться довольно долгое время.
Другим преимуществом программирования является зарплата. Конечно, различные языки программирования предоставляют разные возможности в плане заработной платы, но программирование в целом считается одной из самых прибыльных профессий в мире. Это правда, что обучение и получение опыта в сфере программирования требует много времени, но затраченное вами время явно стоит заработной платы, которую вы будете получать.
Если вы не хотите изучать программирование только из-за карьерных возможностей, то эти навыки могут стать отличным дополнением к вашей текущей работе. Во-первых, предлагаемая программированием гибкость не сравнима ни с чем – знания хитростей и сложностей кодинга откроет перед вам множество дверей – начиная от работы на фрилансе и заканчивая созданием ваших собственных проектов. Кроме того, обучение программированию поможет вам стать более дисциплинированным и терпеливым!
Итак, мы уже познакомились с преимуществами сферы программирования, настало время перейти к основной теме нашего руководства C vs C++. Однако перед этим, давайте расскажем о каждом языке в отдельности. Начнём с C.
Самые Полюбившиеся Статьи
Ищете более подробную информацию по какой-либо связанной теме? Мы собрали похожие статьи специально, чтобы вы провели время с пользой. Взгляните!

Лучшие Онлайн Курсы Психологии 2024: Детальное Руководство
Приобретайте знания для жизни и бизнеса, изучая курсы психологии — ТОП 7 платных и бесплатных вариантов.
Специализация Coursera Глубокое Обучение: Почему Настолько Популярна?
Как глубокое обучение связано с биологией? Почему бы не изучить её? Взгляните на лучшие курсы Coursera Глубокое Обучение!

Фреймворки JS: Узнайте Ваши Варианты
Фреймворки JS делают работу разработчиков гораздо легче — если выбраны правильно. Узнайте про лучшие JavaScript фреймворки в нашем руководстве!
C

C — это системный язык программирования общего назначения. Даже само определение говорит о том, что данный язык уникален и выделяется среди других популярных языков программирования. Каким образом? Просто являясь системным языком программирования. Это означает то, что C чаще всего используется для программирования системного обеспечения, например, программы самого компьютера. Другими словами, C используется для создания программ, которые затем используются для работы компьютера. Это в какой-то степени объясняет тот факт, что C считается одним из самых сложных языков программирования в мире.
C был создан и разработан в 1972 году компьютерным специалистом Деннисом Ритчи. С тех пор он стал самым культовым языком программирования. Как мы упомянули ранее в этом руководстве по C vs С++, многие люди до сих пор имеют мнение, что вне зависимости от контекста, C должен быть первым языком программирования для изучения. Эту идею в основном поддерживают ветераны индустрии – несмотря на то, что в данный момент трендом являются более простые для изучения и удобные языки программирования, эти люди считают, что при подобном подходе вы теряете все возможности развить ваше логическое мышление и упускаете саму суть программирования.
Если мы говорим про отличие С от С++, то для начала стоит отметить тот факт, что C++ является одним из ответвлений C. Это означает, что C можно назвать прародителем языка C++ и последний унаследовал от него основные особенности.
Итак, чтобы принять решение относительно нашего выбора между C или C++, давайте теперь поговорим про C++.
C++

Также как и C, C++ — это язык программирования общего назначения. Он был создан в 1979 году (спустя 7 лет после создания C) Бьёрном Страуструпом, и в основном используется для создания сетей, игр и других компьютерных приложений.
Самым главным отличием C++ от C является то, что этот язык программирования (по большей части) является объектно-ориентированным, тогда как C является процедурным. Поскольку C в основном используется для разработки компьютерных систем, то это различие не является критичным, но если говорить об определённом типе разработки (например, разработке компьютерной программы), то C++ будет более применимым, нежели C. Позвольте мне объяснить почему.
Если язык программирования объектно-ориентирован (будь-то полностью или частично), он считается адаптируемым. Это означает, что язык быстр и эффективен, поэтому может считаться одним из лучших. В случае с C vs C++, C++ ориентирован на объекты и данные, нежели действия и логику (что сложилось исторически). Объектно-ориентированное программирование позволяет языку сфокусироваться на самом объекте, игнорируя менее важные детали в процессе.
Хотя в большинстве случаев, C++ очень похож на C. Они оба легки, имеют ручное управление памятью и могут использоваться для программирования практически всего.
Учитывая сказанное, теперь вы должны иметь некоторое представление о каждом из языков. Теперь давайте определим критерии для сравнения C vs C++.
Критерии Сравнения
При сравнении двух языков программирования (будь-то C vs C++ или любых других), есть несколько вещей, на которые нужно обратить внимание. В основном всё это сводится к вашим личным предпочтениям, но вам всё равно нужно иметь некоторую информацию для определения этих предпочтений, ведь так?
Чтобы определить какой язык лучше вам подходит C или C++, мы выбрали три ключевых аспекта, которые важны для любого языка программирования. Конечно, существует гораздо больше аспектов при выборе языка программирования, но мы достигнем объёма книги про Гарри Поттера, если попытаемся охватить их всех в этом руководстве.
Три основных фактора, которые мы выбрали, это скорость, популярность и возможная зарплата. Давайте кратко расскажем о каждом из них, перед тем как перейти непосредственно к сравнению C vs C++.
Скорость
Скорость несомненно является одним из самых важных аспектов языка программирования. Говорим ли мы о C++ или Python, скорость всегда должна быть на первом месте. И этому есть своя причина!
В последнее время было много споров относительно “скорости” языков программирования. Сейчас распространено мнение, что хороший язык программирования должен быть быстрым – как для экономии времени, так и для общей эффективности. С другой стороны, некоторые опытные программисты заявляют, что фактор скорости уже не так важен, как это было, скажем, 10 лет назад. На сегодняшний день современные процессы способны компенсировать недостатки медленных языков. Хотя статистика всё равно говорит иное – самые распространённые языки программирования (среди прочего) обладают очень быстрой скоростью.
Популярность
Да, это не связано с функциональностью языка напрямую, но это действительно важно с другой точки зрения. Для этого существует две основные причины.
Для начала, по крайнер мере, когда это касается сферы программирования, популярность является признаком качества. Если язык программирования популярен, то, скорее всего, он будет… что же, качественным. Так как сфера программирования постоянно развивается и изменяется, то у разработчиков нет времени и мотивации на постоянное преследование последних трендов. По этой причине, только по-настоящему полезные и эффективные языки программирования достигают вершин популярности.
Более того, если язык программирования популярен, то вы можете быть уверены — он обладает огромным сообществом программистов. Это означает возможность постоянного взаимодействия, нескончаемое количество полезной информации (если вы не хотите тратить время на традиционное обучение), различные сообщества и многое другое. Нет ничего более сложного и удручающего, чем изучение нового языка программирования без возможности найти о нём информацию в сети.
Вы когда-либо хотели узнать, какие платформы для онлайн обучения лучше всего подходят для вашей карьеры?
Заработная Плата
Мы уже затрагивали эту тему в начале руководства C vs C++, но различные языки программирования предлагают разные заработные платы. Само различие зависит от нескольких ключевых факторов – сложности языка, его полезности, популярности и насколько востребованным будет данный язык для вашей работы. И несмотря на то, что сфера программирования оплачивается очень хорошо, различие в размерах заработной платы (в зависимости от языка программирования) могут быть очень существенными.
C vs C++
На данный момент, у вас уже должен быть заложен определённый фундамент, чтобы мы смогли сравнить два этих невероятных языка программирования. Ранее в этом руководстве мы успели рассмотреть общий концепт программирования, нашли отличие C от C++, а также определили критерии сравнения.
Чтобы вам было проще, давайте уделим для каждого из критериев отдельный раздел, чтобы провести детальное сравнение C vs C++.
Какой Язык Быстрее?
В последнее время разгорелось множество дискуссий, относительно того, какой язык быстрее C или C++. Хотя чаще всего можно встретить мнение, что C лишь немного быстрее C++. Однако ситуация была совсем иной некоторое время назад – если бы вы задались этим же вопросом пять или десять лет назад, то несомненным победителем был бы C. С современными технологиями разница между этими двумя языками стала очень незначительной.
Какой Язык Популярнее?
Этот аспект очень субъективен и определить явного победителя в противостоянии C vs C++ не так то просто. Оба языка имеют огромную фанатскую базу, а количество информации, доступной в сети, просто невозможно сосчитать.
Преимущества
- Простой в использовании
- Предлагает качественный контент
- Очень открытый в своих ценах
Особенности
- Бесплатные сертификаты об окончании
- Фокус на навыки науки о данных
- Гибкое расписание занятий
Преимущества
- Простой дизайн (без бесполезной информации)
- Хорошее качество курсов (даже бесплатных)
- Разнообразие возможностей обучения
Особенности
- Программа Nanodegree
- Подходит для корпоративного обучения
- Платные сертификаты об окончании
Преимущества
- Огромное разнообразие курсов
- Простая навигация
- Нет технических проблем
Особенности
- Огромное разнообразие курсов
- Политика 30-дневного возврата средств
- Бесплатные сертификаты об окончании
Какой Язык Предлагает Лучшую Заработную Плату?
По данным Payscale.com, средняя зарплата разработчика на C за год составляет $90,000 USD или $7500 USD в месяц. В противоположность этому годовая зарплата разработчика на C++ равна $95,000 USD или почти $7920 USD в месяц (но уже по данным Glassdoor.com).
Несмотря на то, что разница не очень ощутима, кажется, что разработчик на C++ может ожидать более высокую зарплату, чем свой визави. Это можно объяснить тем, что C++ более сложен в изучении и чаще всего используется в индустрии разработки игр. Хотя в общем оба показателя выглядят очень привлекательно!
Самые Популярные Статьи
Взгляните на нашу коллекцию руководств, статей и уроков про платформы для онлайн обучения и массовые открытые онлайн курсы. Всегда будьте в курсе и принимайте взвешенные решения!

Лучшие Udemy Курсы: ТОП 10 Вариантов на Выбор
Вам интересно, стоит ли Udemy вашего внимания? Я здесь, чтобы доказать, что стоит! Взгляните на лучшие Udemy курсы.
Лучшие Курсы Udacity: ТОП 7
Хотите начать обучаться и выбрали Udacity в качестве площадки для обучения? Взгляните на лучшие курсы Udacity и узнайте, какой подходит вам больше всего!

Вопросы Собеседования Informatica: Работа с Управлением Данных
Изучаем вопросы для собеседования Informatica. Интеграция и управление данными в Informatica ETL. Отвечаем на вопросы собеседования.
Заключение
Какую сторону в противостоянии C VS C++ примите вы, целиком и полностью зависит от ваших предпочтений. Каждый из этих языков откроет перед вами массу возможностей – здесь всё сводится к вашим причинам начать изучать программирование.
Надеемся, что данная статья C VS C++ была для вас полезна. Удачи в изучении!
Оставьте ваше честное мнение
Оставьте ваше честное мнение и помогите тысячам людей выбрать лучшую платформу для онлайн обучения. Все отзывы, позитивные или негативные, будут приниматься, если они честны. Мы не публикуем предвзятые отзывы и спам. Поэтому, если вы хотите поделиться вашим опытом, мнением или дать совет — у вас есть всё необходимое!
Почему С++ быстрее многих языков программирования?
Достаточно интересно полностью понять, почему С++ настолько быстр по сравнению с условным Kotlin, Java или C#. Вопрос: Расскажите пожалуйста в деталях, почему именно так) Предполагаю, что это благодаря достаточно гибкой работой с памятью, но кажется, что сильно заблуждаюсь)
Отслеживать
задан 6 мая 2023 в 14:41
118 2 2 серебряных знака 15 15 бронзовых знаков
Потому что он компилируется в машинный код, который выполняется процессором напрямую, а не через виртуальную машину какую-нибудь
6 мая 2023 в 14:43
6 мая 2023 в 14:53
@aleksandrbarakin тут вопрос не про Python
6 мая 2023 в 14:53
@andreymal, какая разница? ответы там есть и они полностью покрывают смысл данного вопроса.
6 мая 2023 в 14:54
@aleksandrbarakin целых три принципиальных разницы: в питоне (подразумеваю CPython) есть GIL, нет статической типизации и нет JIT-компиляации — а в Java и C# всё ровно наоборот
6 мая 2023 в 14:56
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
Во, я наверное хорошо смогу ответить, так как являюсь специалистом в C# и .NET
Но перед тем как вникать в суть, запомните основное правило: тормозит не язык, на котором вы написали код или среда выполнения, тормозит сам код, который вы написали.
И главная проблема плохого тормозящего кода в таких средах со сборщиком мусора, как .NET и Java, это отсутствие экспертизы в области поведения той самой машины управления памятью. Хочешь создать 200кк массивов для выполнения задачи — легко, код прост. Просто спавни объекты и ни о чем не думай, а сборщик мусора разберется.
В таких средах, где нет встроенного управления памятью, и надо его писать по большей части самому, приходится разбираться с этим до того как код дописать, потому что иначе будут утечки и прочие неприятности. Когда учишь С++, изначально познаешь базовые тонкости работы с памятью, а многие новички сразу начинают различать malloc и calloc , и осознавать, что выделение высвобождение памяти не бесплатно по времени и требует работы, переиспользуют память и т.д.
Чтобы научиться контролю аллокаций в средах типа .NET, требуется сначала написать много тормознутого кода, и наконец-то встретиться с проблемой низкой производительности или перерасхода памяти лицом к лицу. То есть вникание в управление памятью и суть работы сборщика происходит гораздо позднее. Поэтому при одинаковом объеме обучающих материалов, специалист по C++ узнает про нюансы производительности раньше, чем специалист по C#. И это нормально, это специфика среды, в которой работает код.
Поэтому вопрос «что быстрее, С++ или C#/Java» изначально не имеет смысла. Даже если столкнуть лбами двух супермегаэкспертов C++ и C# — неизвестно, чей код будет быстрее. Напишешь на C++ код, потом начнешь тестировать, на одном сервере победишь C#, на другом в другой архитектуре — проиграешь. Ну потому что JIT во время компиляции знает все тонкости процессора, выполняющего код, а компилятор C++ — не знает, он знает только архитектуру. Есть еще PGO (Profile-Guided Optimization) в CLR (Common Language Runtime/.NET) — великолепная шутка, которая может перекомпилировать с сильными оптимизациями код уже запущенного приложения в зависимости от интенсивности использования некоторых методов в коде (речь про последние версии .NET, но раньше ситуация была хуже). В C++ такого либо нет, либо оно достигается значительно тяжелее, чем в среде с JIT. И еще миллионы нюансов, которые никак не ответят на ваш вопрос однозначно.
И здесь можно повторить вывод, который написан жирным шрифтом выше, в самом начале. А утверждение «С++ быстрее многих языков программирования» по факту не соответствует действительности. Я могу на C# написать код, который будет например применять фильтр к Full HD картинке 2 секунды, а могу потом переписать его так, что станет потом 4мс, а качество машинного кода, который выдает JIT, будет не сильно хуже, чем генерит gcc/clang. Не забывайте, что компиляция байт-кода (или CIL) производится машиной в целом однократно, а затем уже работает только скомпилированная версия. Например вызываете какой-то метод 1000 раз, а компилироваться он будет только однажды. JIT — это не интерпритатор байт-кода, а компилятор.