LINUX.ORG.RU

Контейнеры в C


2

3

Будучи сиплюсплюсником, меня давно интересует как те же задачи выполняются в C. Знаю, на ЛОРе есть множество приверженцев C, надеюсь они ответят на пару простых вопросов.

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

Не будем зарываться в дебри boost'а, возьмём простую задачу - контейнеры.

Для примера я взял односвязные списки в GTK GSList и Qt QList. QList позволяет хранить как простые, так и сложные типы с конструкторами, деструкторами, типы с общими данными. При этом накладных расходов на выделение в куче не происходит, а при реаллокации выбирается нужный алгоритм в зависимости от QTypeInfo<T>, к примеру для int будет вызван memcpy(), а для QString оператор копирования. Кроме того блок данных заранее резервируется и для сложных типов вызывается placement constructor. Для удаления данных по указателям используется алгоритм qDeleteAll(from,to), который сам дёрнет нужные конструкторы. Беглый просмотр GSList показал, что в нём можно хранить только указатели, со всеми вытекающими накладными расходами на аллокацию/уничтожение памяти, ручное кастирование, слежение за утечками, фрагментацию памяти.

Аналогичная ситуация со связными списками GList и QLinkedList.

Это даже не затрагивая вопрос о типизации, в C++ компилятор сразу даст по рукам при попытке записать в контейнер неверный тип или с помощью неявного преобразования с explicit-конструктором.

Далее, счётчики ссылок и деструкторы.

К примеру, в C++ список строк будет выглядеть как QList<QString>, при этом можно забыть про внутреннюю структуру строки - конструктор, деструктор и операторы копирования сами занимаются подсчётом ссылок, разделением и уничтожением данных. Вернуть строку из функции проще простого:

QString foo()
{
    return listOfStrings.at( 5 );
}

Этот код абсолютно безопасен и оптимален с точки зрения использования памяти и процессора, поскольку время тратится только на увеличение счётчика и копирование указателя.

В GTK я сходу нашёл GString:

struct GString 
{
  gchar *str;
  gint len;
};

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

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

Или может GTK неудачный пример, тогда дайте ссылку на правильную C-библиотеку с контейнерами.

★★★★★

Ответ на: комментарий от linuxfan

отличный ответ :)
очень знаете тяжело спорить с с++ никами на их поле

ae1234 ★★
()
Ответ на: комментарий от MuZHiK-2

Вначале попробовал отвечать на вопросы товарища MuZHiK-2, но понял, что дело это неблагодарное.

Дабы сбить температуру обстановки попробую уточнить критерии задачи, а то разговор ушёл не в ту сторону.

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

Используемая библиотека на C не играет роли, но поскольку главный защитник этого языка, MuZHiK-2, несколько раз упомянул GList, то предлагаю его и сравнивать. Для сравнения со стороны C++ возьмём такой же связный список - QLinkedList.

Звено GList:

typedef struct {
  gpointer data;
  GList *next;
  GList *prev;
} GList;

Звено QLinkedList:

template <typename T>
struct QLinkedListNode
{
    inline QLinkedListNode(const T &arg): t(arg) { }
    QLinkedListNode *n, *p;
    T t;
};

Как видим в C++ с помощью шаблона ликвидируется накладные расходы на аллокацию лишних данных в куче, в C же всё хранится по указателю.

Идём дальше, сравним сортировку в массивах GArray и QVector:

GList:

void g_array_sort_with_data(GArray *array, GCompareDataFunc compare_func, gpointer user_data);

QVector:

qSort();

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

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

Dendy ★★★★★
() автор топика
Ответ на: комментарий от shelA

>Не понял, это вроде не моя цитата?

Пардон, промахнулся.

Я не спорю, на С динамический полиморфизм организовать можно, но зачем изобретать велосипед если есть в системе С++ компилятор?

Статический полиморфизм = вызов заранее известных методов. Если ты пишешь object1.method1() и object2.method1() на плюсах, то это на мой взгляд однозначно проигрывает по читабельности сишному class1_method1(instance1) и class2_method1(instance2), потому что в плюсовом случае тебе еще надо понять, какой именно метод вызывается, а в сишном все очевидно. Практически самодокументируемый код.

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

linuxfan
()
Ответ на: комментарий от linuxfan

> Виноват язык, который не поощряет использование мозга.

Да што ты говоришь, и на си можно написать такую хрень страниц на десять, хотя реально можно бы было уложиться в страничку, а на плюсах в строчек 10-15 кстати.

в результате получается Mozilla Firefox

Пример в студию хорошего браузера написанного на pure С.)

shelA
()
Ответ на: комментарий от linuxfan

Не верю я насчет 10ти кратного прироста именно из-за такой работы с заголовками. Тебе чтобы только скачать блок заголовков придется либо выделять большой блок памяти, либо делать realloc. Если ты делаешь realloc, то ты потеряешь указатели и не сможешь работать с ними так как ты работаешь. Чего-то ты темнишь. Кстати искать потом в этом массиве методом перебора будешь ?

Reset ★★★★★
()
Ответ на: комментарий от Dendy

вообще внизу всего ассемблерный код

а через какие дебри - мысли программера добираютья до ассемблерного кода - дело языков програмирования и компиляторов

точнее опиши - в чем ты видиш неэффективность Си решения ?

ae1234 ★★
()
Ответ на: комментарий от linuxfan

> По большому счету, эффективно работать со строками можно только на C и именно в силу специфики того, что строка = последовательность байт, завершаемая нулем.

Даээ, а в плюсах как же строка представляется? Небось в виде связанного списка? Или несвязанного?)))

разбор HTTP-заголовков на плюсовке: вычленяем имя заголовка, его значение и пихаем в std::map

С тобой все ясно, если ты когда-то на плюсах так делал, это не говорит о том что и все так же нездоровы )))

shelA
()
Ответ на: комментарий от Dendy

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

struct Person {
    char *nick;
    char name[0];
};

struct Person* make_person(const char *nick, const char *name)
{
    unsigned int nick_l = strlen(nick), name_l = strlen(name);
    struct Person *p = (struct Person*)malloc(sizeof(struct Person*) + nick_l + name_l + 2);
    p->nick = (char*)p + sizeof(*p) + nick_l + 1;
    memcpy(p->nick, nick, nick_l + 1);
    memcpy(p->name, name, name_l + 1);
    return p;
}

А затем в хэш-таблице (или дереве) мы не копируем лишний раз nick, а используем указатель на data->nick. В итоге получаем выигрыш по памяти (не дублируется nick) и по скорости (нет подсчета ссылок, который ой какой небесплатный).

К слову сказать, такой способ хранения данных настолько выгоден, что можно даже потерпеть лишнее разыменование void-указателя во всяких там GList'ах.

linuxfan
()
Ответ на: комментарий от linuxfan

> О том, что в результате получается Mozilla Firefox я уже говорил.

Ну без мозга на Си получится еще хуже.

А вот WebKit работает на ура.

namezys ★★★★
()
Ответ на: комментарий от shelA

>Даээ, а в плюсах как же строка представляется? Небось в виде связанного списка? Или несвязанного?)))

Кросавчег, прежде чем ляпнуть, сначала подумай, почему с плюсовыми строками и их refcounting'ом описываемая конструкция в принципе невозможна. И почему доступа к плюсовой строке как к массиву байт ты не имеешь — только через перегруженный operator[].

Ну и так, задачка на сообразительность: как без копирования в плюсовке создать строку, ссылающуюся на подстроку другой строки, начиная с N-го символа (очень распространенный случай, кстати). При использовании (const) char* str это просто str + N. А с плюсовыми std::string ты побежал вызывать std::string(InputIterator first, InputIterator last).

linuxfan
()
Ответ на: комментарий от linuxfan

Блин я уже не раз слышу от плюсовиков да ваши std::map ... Блин а кто тебя их заставляет сувать куда ни попадя. Все кто их первый раз увидят думают это такой быстрый ассоциативный масивчик и втыкают его куда ни попало (обычно бывшие перловоды и пыхпыхи), потом стонут «ах как это все медленно». Если в плюсах есть std::map - это не говорит о том что его надо воткнуть во все места.))) Все ясно с тобой уважаемый, ты даже не потрудился man stl сделать до того как напихал в код всякого добра типа stl::* )))

shelA
()
Ответ на: комментарий от namezys

>Ну это уже низкоуровневый код.

Превед! А C — это кроссплатформенный ассемблер!

А если кто нить удалит nick выше - то все ляжет раком?

В приципе, const char *nick, const char name[0] тебе в помощь. Мне было лениво касты к char* писать, поэтому без const обошелся.

linuxfan
()
Ответ на: комментарий от namezys

>Не надо ограничиваться достаточно нехорошими строками std::string

Ой, а что надо делать? QString чтоли? Та же бяка, только в профиль и с претензией на всеобъемлющесть.

linuxfan
()
Ответ на: комментарий от shelA

>Блин а кто тебя их заставляет сувать куда ни попадя.

И к чему мы скатываемся? В плюсах нет ничего хорошего готового? Шаг вправо, шаг влево — и гугли библиотеки ассоциативных массивов для плюсов? И чем это тогда отличается от, скажем, libdict?

linuxfan
()
Ответ на: комментарий от linuxfan

> А потом мы сидим и думаем: чего это фаерфокс так медленно рендерит? А он, оказывается, занимается типичным глубоким копированием сложных структур данных совершенно прозрачно от пользователя и программиста.

а ты файерфокс вообще давно запускал? что-то тормазов я уже пару лет как не видел..

Cy6erBr4in ★★★
()
Ответ на: комментарий от linuxfan

> Кросавчег, прежде чем ляпнуть, сначала подумай, почему с плюсовыми строками

Во первых красавчек, раскажи нам тупым плюсовикам как у нас в этих самых плюсах хранятся строки.

А с плюсовыми std::string ты побежал

А во вторых объясни нам, почему нам обязательно надо использовать std::string вместо char* если этого не требует алгоритм.

При использовании (const) char* str это просто str + N. А с плюсовыми std::string

Я понял к чему ты клонишь, но чему ты удивлен нет. (const) char* str - это указатель на байт. std::string - это класс(тип) разницу просекаешь. И если тебе строка не нужна как объект заворачивать ее в std::string просто тупо.

shelA
()
Ответ на: комментарий от linuxfan

на такие const тоже спокойно вызывается и delete и free

ну так это можно достаточно легко сделать и на C++

[code]

class String;

class StringRef { StringRef(const String&); };

[/code]

Стринг пихаешь внутрь, а наружу StringRef. Если методы все статические - то это прекрасно оптимизируется в те же строки на С, но будет более безопасно.

Только это не спасает от того, что если кто-то сохранил этот Ref, а снизу его удалили.

В любом случае без GC требуется некоторый уровень доверия

namezys ★★★★
()
Ответ на: комментарий от linuxfan

> Ой, а что надо делать? QString чтоли? Та же бяка, только в профиль и с претензией на всеобъемлющесть.

Вы о них мало знаете. Там хотя бы есть copy-on-write, аллокация памяти заранее с запасом (кусками по 4, 8, 32, 64 и тд) + достаточно эффективная (память не сразу отдается ОС, а резервируется, чтоб использовать потом)

namezys ★★★★
()
Ответ на: комментарий от linuxfan

> Ну и так, задачка на сообразительность: как без копирования в плюсовке создать строку, ссылающуюся на подстроку другой строки, начиная с N-го символа (очень распространенный случай, кстати).

http://doc.qt.nokia.com/4.7-snapshot/qstringref.html

Dendy ★★★★★
() автор топика
Ответ на: комментарий от linuxfan

>> Блин а кто тебя их заставляет сувать куда ни попадя.

И к чему мы скатываемся? В плюсах нет ничего хорошего готового?

Это готовое надо использовать с умом и представлять себе хотя бы в общих чертах как это там все варится. Если ты хочешь использовать все готовое и не парится совсем - юзай Java, C# а еще лучше Delphi )))

shelA
()
Ответ на: комментарий от namezys

> Не. Ну тут критика уместна. stl не очень хорошая библиотека

Ага, особенно если чел сделает что-то типа std:map<int, char*> и будет уверен, что записанные строки будут автоматом создаваться/удаляться при копировании/удалении и мапа )))

shelA
()
Ответ на: комментарий от namezys

К сожелению stringref работает с подсчетом ссылок

К счастью он работает без счётчика (-:

inline QStringRef::QStringRef(const QString *aString, int aPosition, int aSize)
        :m_string(aString), m_position(aPosition), m_size(aSize){}
Dendy ★★★★★
() автор топика
Ответ на: комментарий от Dendy

ой блин. и точно. значит те же ограничения, что и в вышеуказанном примере. Хранить такой объект нельзя

namezys ★★★★
()
Ответ на: комментарий от namezys

> Хранить такой объект нельзя

Можно, но только вместе с неизменяемым оригиналом и только для чтения. Другими словами, QStringRef выполняет те же задачи, что и голая C-строка.

Dendy ★★★★★
() автор топика
Ответ на: комментарий от Cy6erBr4in

>а ты файерфокс вообще давно запускал? что-то тормазов я уже пару лет как не видел..

Зайди на любой китайский сайт с большим количеством их иероглифов и ужаснись скорости рендеринга.

linuxfan
()
Ответ на: комментарий от shelA

>Во первых красавчек, раскажи нам тупым плюсовикам как у нас в этих самых плюсах хранятся строки.

Какие именно? std::string? QString?

А во вторых объясни нам, почему нам обязательно надо использовать std::string вместо char* если этого не требует алгоритм.

Потому что char* — небезопасно в квадрате: ручное управление памяти и типонебезопасно.

И если тебе строка не нужна как объект заворачивать ее в std::string просто тупо.

То есть как распушать хвост про конкатенацию строк оператором «+», то мы приверженцы std::string, а как запахло жареным, то мы и char* не брезгуем? Определись уже. Хуже плюсовой лапши только миксованная лапша из C и C++.

linuxfan
()
Ответ на: комментарий от namezys

>на такие const тоже спокойно вызывается и delete и free

Ты давно последний раз компилятор в руки брал? -Wall -Werror спасут тебя от подобных глупостей.

В любом случае без GC требуется некоторый уровень доверия

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

linuxfan
()
Ответ на: комментарий от namezys

>Там хотя бы есть copy-on-write

В std::string от SGI емнип тоже refcounting => copy-on-write. Только это не панацея, особенно в многопоточной среде.

память не сразу отдается ОС, а резервируется, чтоб использовать потом

По-видимому, это ты хреново представляешь себе механизмы управления памятью. Поскольку это lor, а не винфак, ограничимся линаксом. Так вот, в линаксе память может «отдаваться ОС» только постранично (4kb). Так что твои «4, 8, 32 и т.д.» огрызки строк куски только захламляют все в случае неизменяемых строк (а таких строк большинство: длительное хранение + конкатенация нужны поразительно нечасто).

linuxfan
()
Ответ на: комментарий от linuxfan

Забудем о Фаерфоксе, вижу дискуссия опять пытается убежать в сторону.

Я всё ещё жду ответа на вопрос, описаный здесь: http://www.linux.org.ru/forum/development/5154097/page5#comment-5155311

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

Хотелось бы услышать подтверждение этих мнений от людей, которые на C собаку съели. Либо, если я глубоко заблуждаюсь - пусть выскажут своё мнеение, при этом желательно его подтвердить примерами кода.

Dendy ★★★★★
() автор топика
Ответ на: комментарий от Dendy

>http://doc.qt.nokia.com/4.7-snapshot/qstringref.html

Извини, куте-4.7 отсутствует на целевой машине. Есть только STL и boost. Твои действия?

Кстати, с чего ты взял, что этот QStringRef не производит копирования? const char* data() однозначно намекает, что копирование имеет место быть, причем самое неприкрытое. А то, что эта дрянь применима для хитрожопой технологии template expressions это так и прет изо всех ее щелей.

В этом треде опять-таки наблюдается низкая квалификация пюсофилов: они не понимают, что лежит за всеми их «прекрасными абстракциями».

linuxfan
()
Ответ на: комментарий от shelA

>Это готовое надо использовать с умом и представлять себе хотя бы в общих чертах как это там все варится.

Так ты определись: на плюсах в итоге больше подводных камней, чем в C или меньше? Если больше, то о чем холивар? Плюсы слили.

linuxfan
()
Ответ на: комментарий от linuxfan

С того, что это указано в документации (если кто-то код не умеет читать)

namezys ★★★★
()
Ответ на: комментарий от linuxfan

> Так ты определись: на плюсах в итоге больше подводных камней, чем в C или меньше? Если больше, то о чем холивар? Плюсы слили.

В питоне их еще больше.

namezys ★★★★
()
Ответ на: комментарий от Dendy

>Я всё ещё жду ответа на вопрос, описаный здесь

Вопроса по ссылке не нашел, но предполагаю, что речь по-прежнему идет о контейнерах.

С /usr/include/sys/queue.h /usr/include/sys/list.h и /usr/include/sys/tree.h уже ознакомился? С libdict (хэши, rb деревья + некоторая абстракция над ними) ознакомился? Все еще спрашиваешь, как в C управляются с линейными массивами? Просто и руками. На крайняк GArray, который даже в GLib'е эффективен. Как он эффективен? Смотри на garray_new и думай, для чего нужен параметр «element_size».

linuxfan
()
Ответ на: комментарий от linuxfan

> Какие именно? std::string? QString?

В любой)))

Потому что char* — небезопасно в квадрате: ручное управление памяти и типонебезопасно.

А почему ты его в си юзаешь, а я в плюсах не могу? Считаешь себя умней других?

То есть как распушать хвост про конкатенацию строк оператором «+», то мы приверженцы std::string, а как запахло жареным, то мы и char* не брезгуем? Определись уже.

Если тебя забавляет str1+str2 юзай std::string, если хочешь парсить строку как массив байт юзай char*. Я не брезгую ничего, я смотрю что более выгодно в данной ситуации.

Хуже плюсовой лапши только миксованная лапша из C и C++.

Я так понял использование в С++ char* есть «миксованная лапша из C и C++». Ну,ну я посмеялсо)))

Тебе самому не стыдно за столь тупые аргументы? Где кстате ссылка на быстрый и безглючный браузер на Си. А то в мозиллу какашками покидал, а ответить нечем.

shelA
()
Ответ на: комментарий от namezys

А в Си с их явными преобразованиями типов - так вообще вагон грабель

namezys ★★★★
()
Ответ на: комментарий от namezys

>Это еще почему?

Потому что когда два потока начинают обращаться к такой строке, у них возникают проблемы со счетчиком ссылок, значение которого нужно синхронизировать в различных ядрах. Дальше начинается черная магия по синхронизированию содержимого приватных кешей и задержки-задержки-задержки. Или ты думал, что lock + xadd это совсем-совсем бесплатно?

linuxfan
()
Ответ на: комментарий от linuxfan

>> Это готовое надо использовать с умом и представлять себе хотя бы в общих чертах как это там все варится.

Так ты определись: на плюсах в итоге больше подводных камней, чем в C или меньше? Если больше, то о чем холивар? Плюсы слили.

Понимание того как работает либа, ты считаешь знанием подводных камней? Сформулируй понятие «подводные камни», боюсь мы этот термин по разному понимает.

shelA
()
Ответ на: комментарий от namezys

Если мы можем бороться за такты - то можно об этом думать

Но это редко надо

namezys ★★★★
()
Ответ на: комментарий от linuxfan

ИМХО быдлокод

struct Person { 
    char *nick; 
    char name[0]; 
}; 
 
struct Person* make_person(const char *nick, const char *name) 
{ 
    unsigned int nick_l = strlen(nick), name_l = strlen(name); 
    struct Person *p = (struct Person*)malloc(sizeof(struct Person*) + nick_l + name_l + 2); 
    p->nick = (char*)p + sizeof(*p) + nick_l + 1; //<-- тут ошибка, вместо nick_l надо name_l
    memcpy(p->nick, nick, nick_l + 1); 
    memcpy(p->name, name, name_l + 1); 
    return p; 
}
А что мне помешает подобное написать на С++? Религия?

pathfinder ★★★★
()
Ответ на: комментарий от pathfinder

>//<-- тут ошибка, вместо nick_l надо name_l

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

pathfinder ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.