Comments 67
Да, знаю про это. Как-никак один из важнейших нюансов при написании динамического массива. :)
Нет, у меня именно что происходит увеличение в 1.5 раза. Причём коэффициент можно изменить, переопределив GVEC_GROWTH_FACTOR при компиляции библиотеки.
Значение 1.5 выбрано из-за тех самых тонкостей с оптимальным выбором. Вот хороший ответ на SO, после прочтения которого мне стали понятны преимущества: ссылка.
Но поднять эту тему в таком топике полезно в любом случае.
The values above may seem a bit strange, but here are the guiding principles:
QString allocates 4 characters at a time until it reaches size 20.
From 20 to 4084, it advances by doubling the size each time. More precisely, it advances to the next power of two, minus 12. (Some memory allocators perform worst when requested exact powers of two, because they use a few bytes per block for book-keeping.)
From 4084 on, it advances by blocks of 2048 characters (4096 bytes). This makes sense because modern operating systems don't copy the entire data when reallocating a buffer; the physical memory pages are simply reordered, and only the data on the first and last pages actually needs to be copied.
procedure TList.Grow;
var
Delta: Integer;
begin
if FCapacity > 64 then Delta := FCapacity div 4 else
if FCapacity > 8 then Delta := 16 else
Delta := 4;
SetCapacity(FCapacity + Delta);
end;
Если больше 64 элементов — увеличиваем в 1.25 раза, иначе если больше 8, то на 16, иначе на 4. Дешево и хорошо работает.
Мы как-то нарвались на то, что очереди стали настолько большие, что realloc занимал порядка секунды (с использованием свопинга на диск). И чем больше времени занимал realloc, тем больше накапливалось данных (приложение было шибко многонитевое) и раздувало очередь.
В итоге именно для этой очереди пришлось ввести ручное управление.
Этот комментарий и два предыдущих окончательно убедили меня в том, что надо было делать возможность указывать собственную функцию роста.
Единственная проблема в том, что написание таких возможностей не было основной целью создания этой библиотеки. Однако я буду рад, если кто-нибудь кинет мне PR, реализующий это. :)
Или иметь возможность вручную увеличить размер.
Не заметил сразу.
Это есть, кстати говоря. См. gvec_resize()
и gvec_reserve()
.
Наверное это немного оффтопик, но разве в таких ситуациях unrolled linked list не будет лучшим решением?
Связанные списки мы не использовали, а вот массив (вектор) со страничной организацией у нас был. Почти то же самое, только в заголовке — вектор указателей на страницы. Причина в том, что мы не хотели лишать себя быстрого произвольного доступа.
Может и зря не хотели — произвольный доступ все-таки редкость. Но нам приятней было при обработке обращаться с контейнером как с массивом (доступ по индексу). А не заводить итераторы.
Простите, если заголовок желтоват — у меня родители журналисты как-никак. :)
Похоже, что его действительно нет, а жаль.Ни разу не жаль. С — это язык в котором из фиксированного количества строк на входе получается фиксированное же количество байт на выходе. Коэффициент может быть довольно велик, но, в любом случае — комбинаторный взрыв возможен либо при работе препроцессора, либо оптимизатора.
В С++ же несколько строе кода могут породить мегабайты! С этим, как бы, тоже можно жить — но это уже другой язык с совсем другими свойствами и подходом!
P.S. Котейнеры, кстати, в подобных языках тоже возможны (см. Ada или Java), но с весьма серьёзными ограничениями…
В C — этот код видно. Глазками. И можно переработать реализацию зачастую так, чтобы его стало гораздо меньше. В C++ — его не видно! И нужно производить целые изыскания, чтобы понять откуда что взялось…
Как я сказал: это не упрёк C++ и не похвала C. Это просто объяснение того, где их разработчики сделали принципиально разный выбор.
Чем, там, занимаются специалисты по Computer Science? Когда я был студентом, мне попадались специальные статьи, пестрящие всякими формализмами (теоретико-множественными и категорными), описывающие всякие системы сортов, иерархии типов и т.д. и т.п.
Где же теории, которые позволяют просчитать варианты и определить наилучший (в данных условиях и для данных обстоятельств)?
Чем, там, занимаются специалисты по Computer Science?Хороший вопрос.
Где же теории, которые позволяют просчитать варианты и определить наилучший (в данных условиях и для данных обстоятельств)?Теории есть, но не очень понятно как это всё соотносится с реальностью.
То что реально используется — не есть следствие каких-то глубоких теорий, а следует из простых наблюдений над практическим кодом. Возьмите тот же ICF — думаете кто-то копался в категориях? Да нифига: сделали и посмотрели — есть эффект или нету.
Идея довольно простая: есть хедер без стража включения, который содержит шаблонный код. Этот хедер предполагает, что параметры шаблона установлены извне в макросы с именами вроде TYPE, SIZE, и т.п.. Далее можно включать этот хедер много раз, каждый раз устанавливая эти макросы по-разному: получится несколько инстанциирований «шаблона».
Пример приведён здесь:
http://stackoverflow.com/a/17670232/556899
Этот механизм никак не мешает MSVC: код можно нормально отлаживать, в нём работает intellisense, и т.п. Естественно, нет никаких проблем с возвращаемыми значениями и указателями на функции. Код безусловно разбухает, но ровно на столько же, насколько разбухает аналогичный код с шаблонами C++.
Код безусловно разбухает, но ровно на столько же, насколько разбухает аналогичный код с шаблонами C++.
Или больше, в зависимости от того, насколько аккуратно используется этот трюк. Всё-таки шаблоны инстанцируются тогда и только тогда, когда они либо востребованы, либо инстанцированы вручную.
п.с. никогда не понимал фанатов чистого си, критикующих плюсы. Неужели иметь пару-тройку (сотен) дополнительных инструментов и пользоваться ими — плохо?
Для тех, кому нужна высокая производительность, особенно полезны в C++ именно шаблоны: это абстракция без потери производительности.
Я думаю, это в основном самодисциплина: ограничивая себя в инструменте, люди настраивают себя на определённый стиль программирования.
Очень часто, когда используется C++, люди вовсю используют контейнеры STL, чтобы не думать о работе с памятью, в результате начинаются тормоза из-за огромного количества аллокаций. Если же ограничить себя чистым C, придётся потратить больше времени на продумывание структур данных в памяти, в результате код наверняка будет работать быстрее.
В среднем на три строки кода на си нужна лишь одна строка на с++. Не знаю как вы, а я ценю в коде экспрессивность. А если человек, читающий мой код, не знает, что такое std::bind или др. — это вопрос его квалификации.
Вообще, уже сколько раз было: чем проще язык тем более он распространён.
Вы можете нанять одного профессионала или трех студентов. Профессионал на с++ будет знать свой инструмент, а студенты зная си не будут знать как на нем писать. И с распространенностью так же
В среднем на три строки кода на си нужна лишь одна строка на с++. Не знаю как вы, а я ценю в коде экспрессивность.Ах, вашими бы устами…
Я участовал в проектах где люди использовали C++ ради экспрессивности, но когда я предлагал короткое, буквально однострочное решение — меня просили превратить его в 20 строк, которые «понятнее».
Вот для таких людей С — может быть лучшим вариантом.
А если человек, читающий мой код, не знает, что такое std::bind или др. — это вопрос его квалификации.А если этот человек — ваш тимлид? И он говорит, что лучше заменить
result = func(...) - 1;
на if (func(...)) {
result = 0;
} else {
result = -1;
}
для «наглядности»? Случай из совсем недавнего прошлого, если что…result = func(...) — 1;
если { func(...) } -> bool, то лично я предпочту написать через тернарник.
bool foo();
int bar() {
return foo() ? 0 : -1;
}
int baz() {
return foo() - 1;
}
$ g++ -S -O3 test.cc -S -o-
.file "test.cc"
.text
.p2align 4,,15
.globl _Z3barv
.type _Z3barv, @function
_Z3barv:
.LFB0:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
call _Z3foov
xorl $1, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
movzbl %al, %eax
negl %eax
ret
.cfi_endproc
.LFE0:
.size _Z3barv, .-_Z3barv
.p2align 4,,15
.globl _Z3bazv
.type _Z3bazv, @function
_Z3bazv:
.LFB1:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
call _Z3foov
movzbl %al, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
subl $1, %eax
ret
.cfi_endproc
.LFE1:
.size _Z3bazv, .-_Z3bazv
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
Что случилось с «экспрессивностью», за которую вы ратовали буквально несколькими строками выше?
В конкретном примере экспрессивность в другом заключается. В варианте через тернарник мне не надо смотреть, возвращает foo() bool, int или вообще указатель, но я сразу вижу, что bar() может вернуть только одно из двух значений, 0 или -1, соответственно.
result = bool(func(...)) - 1;
. Правда так будет казаться что func
— изначально возвращает не bool.И, главное, как только мы начинаем обсуждать подобные вещи — весь выигрыш от использования мощного языка, на котором писать быстрее — пропадает. Так что почему не выбрать более простой язык где подобных споров будет меньше?
не надо беспокоится что через жалких 10 лет код превратится в тыквуЧто называется «не дождётесь». И в spec2000 и в spec2006 есть программы на C и на C++. Поскольку это тесты, то, понятно, они с тех пор не менялись. Так вот, вы не поверите — но при сборке на современных платформах проблемы возникают с всякими дурацкими программами на C, а не с программами на C++. Потому что они «слишком много знают» и, например, объявляют сами для себя «extern malloc()» прямо посреди функции какой-нибудь.
С++ постоянно усложняется, это же путь в никуда и ни за чем.C++ усложняется чтобы упростить написание кода. Те же лямбды — штука очень полезная, если их применять вдумчиво. Немного неприятно, что вещь, изобретённая в 60е годы пришла в C++ через полвека после её изобретения, но… Лучше поздно, чем никогда!
Я к плюсам отношусь с большим уважением сейчас, потому что они сделали что обещали: когда C++ сделал поворот в сторону «мегабайт абстракций» в конце прошлого века это было сделано под соусом «оптимизирующий компилятор всё лишнее вычистит» — но это тогда нифига не работало. С современными компиляторами — работает и очень хорошо работает.
Мне проще читать макросы чем шаблоны.
Ключевое слово — «мне». Вы критикуете с++ за то, что они не читаются знанием одного лишь си. А на самом деле у вас попросту нет квалификации в с++.
Не вижу смысла 100500 раз перечитывать код, оно раз оформляется в функцию и потом используется годами в виде опять же одной строчки повсеместно.
Простой пример. У вас есть const char * на входе. Вам нужно дернуть winapi, принимающий LPCWSTR. Напишите интерфейс функции преобразования кодировки на си так, чтобы уместить весь вызов в одну строку и не словить утечку памяти.
У вас есть с++, напишите ядро ОС так чтобы оно работало на голом железе.Вот, пожалуйста. Там, конечно, не всё на C++, так как не с нуля начинали, но вот вам, к примеру, работа с PCI Express.
Я не хочу забивать голову барахлом специфичным для языка, мне важна программа.
Дык, "сишное барахло" тоже является "барахлом специфичным для языка".
На макросах, и что?
Еще скажите, что читать макросы проще, чем шаблоны
1 строчка или 10 — ничего не меняет по сути, уже давно существуют функции и отдельные файлы.
в одном случае читать одну строку, а в другом — 10. Вот и вся разница. Если не надо ломать голову, что делает одна строка, то она всегда будет понятнее любых десяти.
Любой кусок кода за время жизни программы читается в несколько раз больше, чем пишется. А «просто работающий», но нечитаемый код живет ровно до того момента, когда понадобится хоть что-то в нем поменять. Если его ревью пропустит.
Да и, вообще, все эти «холивары» изначально лишены всякого смысла. Хороший программист напишет хорошую программу на том языке, который он знает. Какой знает, так и знает. Но, уж, если знает, то знает хорошо. Плохой программист не знает ни одного языка и, соответственно, ни на одном языке не сможет ничего путного написать.
Так что, на этом, попрошу всякий «холивар» на Хабрахабре прекратить. Спасибо за понимание. ;-)
А я думал, на любом языке программирования, а те, кто знают только один язык, программистами вообще не являются, только кодерами.Не бывает. Нельзя вот так просто взять язык, прочитать мануал к нему и начать «фигачить» хорошие программы.
Любой язык требует освоения. Да, у хорошего программиста это получается достаточно быстро, но всё равно не мгновенно.
Причем основные идеи там практически как у вас.
Не могли бы Вы (пока) прояснить смысл пп. 5 («Совместимость между векторами разных типов на уровне присваивания одного другому») и 7 («Максимальная схожесть интерфейса вектора с таковым у std::vector из C++11.»)?
Допустим, у нас есть вектор, содержащий целые значения, и вектор содержащий числа двойной точности. Что будет означать попытка присвоить один вектор другому? Мы хотим, просто, скопировать значения из одного массива в другой и сделать так, чтобы копия автоматически поменяла бы тип?
Возможно, чтение кода даст мне ответ на мой вопрос, но, было бы неплохо, услышать по этому поводу какие-либо пояснения от Вас. Буду заранее благодарен.
Благодарю.
Статья изначально предполагалась размера примерно в полтора раза большего, чем она вышла в итоге. Очень хотелось описать ещё следующие вещи:
- конкретные детали реализации специализирования макросами, в частности то, как именно реализовано указание передачи и возврата по ссылке или по значению;
- статический и модульный подходы к использованию библиотеки (и их поддержка ею самой);
- почему последний коммит на момент публикации — это монстр под названием "Полностью изменён интерфейс вектора".
Однако было решено не заниматься переписыванием ReadMe из репозитория. К тому же, для иллюстрации потребовались бы большие куски кода. Это всё раздуло бы статью и сделало бы её менее приятной для чтения, и это при живом-то наличии вроде аккуратного репозитория.
Смысл пункта (5) прост: если типы значений в двух разных векторах имеют одинаковый размер, то мы можем прочитать вектор одного типа как вектор другого, и наоборот. У меня был конкретный пример использования, где это было полезно.
Пункт (7) подразумевает, что вектор ведёт себя аналогично std::vector
из C++11, а набор его функций идентичен (пусть и слегка неполон). Я в самом деле сидел с открытым cppreference.com и реализовывал поведение вектора и его хранилища так, как описано там.
Допустим, у нас есть вектор, содержащий целые значения, и вектор содержащий числа двойной точности. Что будет означать попытка присвоить один вектор другому? Мы хотим, просто, скопировать значения из одного массива в другой и сделать так, чтобы копия автоматически поменяла бы тип?
Описанное Вами — это по сути изменение размера вектора плюс один цикл по элементам. Такую задачу в виде функции можно решить только определив callback присваивания. С другой стороны, более элегантно с точки внешнего вида кода пользователя будет вариант с макросом, которому будут переданы массив назначения и исходный массив. Однако такой вариант будет заметно выбиваться из изначальной концепции "не делаем функции макросами".
Я честно пока не решил, какой из этих вариантов лучше, а поскольку это не входило в необходимый мне (и в итоге реализованный) минимум возможностей, то я оставил это на потом. Быть может, я потом напишу отдельно заголовочные файлы для поддержки подобных вещей. Также были планы дописать отдельно некоторые дополнительные функции для использования этого вектора в качестве динамической строки. Есть намётки и на систему callback'ов, и на итераторы. Но — не сейчас.
Тем не менее, я буду рад появлению в репозитории issue, посвящённых таким вещам, а также pull request'ам.
Ну и пару слов в защиту Си по поводу «двадцать первому веку кроссплатформенный ассемблер не нужен» — кроссплатформенных ассемблеров не бывает, это противоречит самому определению ассемблера. Си — это язык, оперирующий основными конструкциями, присутствующими на аппаратном уровне в большинстве современных компьютерных платформ, но скрывающий детали реализации. Именно поэтому он очень хорош для написания ПО встроенных систем, драйверов устройств, некоторых модулей операционных систем, и реальной замены ему в этих задачах — нет (ну или можно на C++ писать как на чистом С, что то же самое).
Спасибо.
А про "кроссплатформенный ассемблер" — это же ирония. Там всё предисловие ею заляпано.
В результате таких раздумий мог бы появиться, например, новый язык программирования, который можно было бы обозвать «++C» — язык, в котором, при сохранении прозрачной семантики Си, существенно изменён синтаксис таким образом, чтобы обеспечить, например, прозрачную и сквозную типизацию данных путём упрощения языковых конструкций и предоставления специальных средств управления типами (вроде RTTI, атрибутов и рефлексии).
Во-вторых, я пропустил слишком много серий, и мне было бы крайне любопытно узнать о последних нововведениях. Но, даже, их наличие не может заставить нас отказаться от острых экспериментов над кодом, особенно, если будет очень важен побочный эффект. Кто знает, куда может нас завести жажда познания (и самопознания)?!?
Напрмер: создание массива struct Person, добавление элементов и обход в цикле.
#include <stdlib.h>
#include <stdio.h>
#include "genvector/genvector.h"
typedef struct person_s {
char Name[32];
int Age;
} person_t;
GVEC_INSTANTIATE( person_t, person, GVEC_USE_VAL, GVEC_USE_REF );
int main() {
gvec_person_t family = gvec_person_new(3);
gvec_person_push( &family, (person_t){
.Name = "Alice",
.Age = 30
} );
gvec_person_push( &family, (person_t){
.Name = "Bob",
.Age = 32
} );
gvec_person_push( &family, (person_t){
.Name = "Kate",
.Age = 10
} );
printf( "%zu\n", gvec_count( family ) );
while ( gvec_count( family ) > 0 ) {
person_t member = *gvec_person_back( family );
printf( "name %s, age %d\n", member.Name, member.Age );
gvec_pop( family );
}
gvec_free( family );
}
Прошу прощения, но не силён в примерах. Целью было именно описание примерного хода мыслей при создании библиотеки.
К тому же, для приведения примеров пришлось бы описывать оба реализованных подхода к использованию библиотеки (статический и модульный), что сильно бы увеличило размер статьи.
1) Примеры очень нужны. И желательно в самой статье. Не обязательно их подробно расписывать. Можно просто спрятать исходник под спойлер — кому нужно, тот почитает.
2) Также хотелось бы пример с доступом к элементам вектора как к элементам обычного массива. Т.е. могу просто сделать family[i].Name = "Batman"
?
1) Добавил пример из сообщения выше в статью.
2) Да, но нет, потому что присвоить строковой литерал нельзя — мы в Си. :)
Но можно сделать вот так, да:
strcpy( family[2].Name, "Batman" );
Аналог std::vector из C++11 на чистом C89, и как я его писал