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-библиотеку с контейнерами.

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

даже если написать так: list.append(person(dendy, i));

Где здесь выделение памяти в куче? Я думал что внутри QList вызывается конструктор копирования, и person копируется во внутренний массив, заранее выделенный.

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

Где здесь выделение памяти в куче? Я думал что внутри QList вызывается конструктор копирования, и person копируется во внутренний массив, заранее выделенный.

void **QListData::append(const QListData& l)
{
    Q_ASSERT(d->ref == 1);
    int e = d->end;
    int n = l.d->end - l.d->begin;
    if (n) {
        if (e + n > d->alloc)
            realloc(grow(e + n));
        ::memcpy(d->array + d->end, l.d->array + l.d->begin, n*sizeof(void*));
        d->end += n;
    }
    return d->array + e;
}
ahonimous
()
Ответ на: комментарий от anonymous

> Я думал что внутри QList вызывается конструктор копирования, и person копируется во внутренний массив, заранее выделенный.

Именно так работает QVector. В QList есть reserve, который позволяет сэкономить на реаллокации буфера под указатели, если заранее известно, что элементов будет много.

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

> В QList есть reserve

только с 4.7, все-таки они странные - взять за пример STL и не реализовать сразу такие базовые и нужные вещи как reserve и resize

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

> только с 4.7, все-таки они странные - взять за пример STL и не реализовать сразу такие базовые и нужные вещи как reserve и resize

QList ни разу не используется в коде, критичном к скорости. Это не специализированный список под конкретные нужды, а универсальный клей на все случаи жизни, использующийся в API самого Qt. Он усреднённо-оптимальный. Здесь вам и выбор алгоритма аллокации данных, и быстрое добавление/удаление в начало/конец списка, и доступ по индексу за константное время.

Для алгоритмов, критичных к скорости QList'у есть замена - QVector. Здесь вам и резервирование в куче, и заполнение, быстрый доступ по индексу, прямой доступ к памяти, хранение данных по значению.

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

Да. Недостаток. Но при этом QList это смесь list и vector. Получается хорошее и оптимальное решение

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

> Я думал что внутри QList вызывается конструктор копирования, и person копируется во внутренний массив, заранее выделенный.

Заранее выделенный на стеке? в 10 000 000 элементов? Даже такая элементарная логика должна подсказать, что это невозможно.

Уж не говоря о том, что если подумать, то можно понять, что такое решение невозможно. На стеке выделяется память при конструировании объекта. Для этого есть вроде QFastArray с параметром шаблона о задании размера базового массив

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

так дело не пойдет, в Си ты одним способом меряешь время, а в лиспе другим.

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

и видел и отлаживал, поэтому Вас и спрашиваю что же там не так :)

Я даже не знаю, с чего лучше начать: с дико замусоренного текста сообщений об ошибках

во-первых текст сообщений об ошибках не является частью стандарта языка, не?

во-вторых выхлоп в большинстве компиляторов не сильно сложнее сообщений об обычных ошибках, да много и непривычно, но не смертельно

с неприемлимого времени компиляции.

для меня время компиляции приемлемо (побольше памяти, потолще проц), если Вам так критичен данный показатель - используйте интерпретируемые языки, там вообще компиляция не обязательна

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

У меня есть примеры веб-сервисов, которые сначала писались «не напрягая мозг» на це-пе-пе, а потом с минорным напряжением мозжечка на C. При этом 10-кратная разница в производительности и 5-кратная в потребляемой памяти. Не говоря уже об отсутствии ошибок и падений в сишном случае. И это все достигнуто исключительно благодаря тому, что в сишном случае я знал, что я пишу, а в плюсовом человек городил паттерны из умных книжек.

эм, а в описанной ситуации Вы чего-то другого ожидали? поменяйте местами С и C++ и то же самое будет

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

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

пруф будет?

Как выглядит разбор HTTP-заголовков на плюсовке: вычленяем имя заголовка, его значение и пихаем в std::map (минимум 3 malloc'a + 2 копирования строк). Как это выглядит на C: записываем в массив пару указателей, заменяем ':' и '\r' (или '\n') на 0. Вот тебе и 10-кратный выигрыш в производительности в одном из часто используемых мест в программе.

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

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

> Как выглядит разбор HTTP-заголовков на плюсовке: вычленяем имя заголовка, его значение и пихаем в std::map (минимум 3 malloc'a + 2 копирования строк). Как это выглядит на C: записываем в массив пару указателей, заменяем ':' и '\r' (или '\n') на 0. Вот тебе и 10-кратный выигрыш в производительности в одном из часто используемых мест в программе.

только недавно на ЛОР показывал как я на С++ делаю точно так же, вот только на С++ можно решать как сделать - быстро и удобно, или очень быстро, но с жонглирвоанием указателями

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

>поменяйте местами С и C++ и то же самое будет

Не будет. На цэ такое будет просто сложно написать, а вот на цэпэпэ это достаточно легко: тут std::map, там boost::thread, здесь еще какая-нибудь бяка и в итоге имеем то, что имеем.

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

> а вот на цэпэпэ это достаточно легко: тут std::map, там boost::thread, здесь еще какая-нибудь бяка и в итоге имеем то, что имеем.

насчет std::map - никто из сишников так и не предоставил сравнимый по скорости вариант, так что...

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

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

4.2

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

без копирования - никак, так же как и в голых сях :) (CoW отложим в сторонку пока)

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

>на С++ можно решать как сделать - быстро и удобно, или очень быстро, но с жонглирвоанием указателями

Разница в том, что на C это будет «очень быстро, удобно и с указателями».

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

да просто писать буду час. а потом еще 2 часа искать, где надо free поставить

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

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

и что в них нехорошего (за вычетом отсутствия юникода)?

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

> Разница в том, что на C это будет «очень быстро, удобно и с указателями».

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

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

Читай по буквам

>насчет std::map - никто из сишников так и не предоставил сравнимый по скорости вариант, так что...

libdict

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

> и что в них нехорошего (за вычетом отсутствия юникода)?

копирование буфера при копировании строки. нет оптимизации работы с памятью

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

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

Не используй метафоры в повседневной речи. У тебя это плохо получается.

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

stl не очень хорошая библиотека

пруф будет?

намекаю Вам что реализаций stl больше чем 1, и больше чем 2

shty ★★★★★
()
Ответ на: Читай по буквам от linuxfan

libdict

	dict *dct;
	dict_itor *itor;

        dct = rb_dict_new((dict_cmp_func)strcmp, free, free);
	itor = dict_itor_new(dct);
	for (; dict_itor_valid(itor); dict_itor_next(itor))
		printf("%s", dict_itor_key(itor));
	dict_itor_destroy(itor);

	dict_destroy(dct, TRUE);

и это вместо

map<string,string> dct;
for( string& it : dct ) cout << it;

и при этом код на С++ будет лучше соптимизирован за счет разворачивания шаблонов

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

Он STL придумал когда ядовитой рыбой Фугу отравился, в шизофреническом бреду, натурально.

тебе завидно, да?

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

Оки. Задача: адресная книга

Одна запись: имя + телефон

Надо заполнить таблицу из n элементов с именем вида «name_k» и телефоно k

затем все пробежать и считать. Потом выбрать из этой таблицы элементы с name_k, где k = 0, 5, 10... прямо из таблицы

затем изменить name_k, где k = 0..10 на name_k_new

k, name - читаем из опций программы (чтоб не дать схалявить компилятору)

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

Это уже столько раз всеми расписывалось, и постоянно расписывается, что мне лень. Для начала, сюда сходи: http://yosefk.com/c fqa/index.html

ну и стоило приводить здесь исповедь неосилятора?

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

поменяйте местами С и C++ и то же самое будет

Не будет. На цэ такое будет просто сложно написать, а вот на цэпэпэ это достаточно легко

ещё как будет, сам видел :) Вы просто не видели как пишут люди которые достаточно плотно ковырялись с С++, но не видели С (есть, есть такие)

вот на цэпэпэ это достаточно легко: тут std::map, там boost::thread, здесь еще какая-нибудь бяка и в итоге имеем то, что имеем.

Вы своё незнание и отсутствие опыта не подавайте за недостатки языка

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

пруф будет?

Пруф был выше по тексту в функции make_person. Аналога с плюсовыми строками создать не получится.

чёт я там не увидел ничего уникального и нужного одновременного, может просмотрел?

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

и что в них нехорошего (за вычетом отсутствия юникода)?

std::wstring - UTF32

во-первых речь шла о std::string

во-вторых до недавнего времени у std::wstring имелись серьёзные косяки в реализации (в win wchar_t - 2 байта, в lin - 4, как хочешь так и ешь)

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

> и что в них нехорошего (за вычетом отсутствия юникода)?

копирование буфера при копировании строки.

use case пожалуйста

нет оптимизации работы с памятью

это Вы про что?

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

Мне ее интерфейс не слишком радует

маловато пруфа для высказывания «stl не очень хорошая библиотека»

интерфейс какой библиотеки Вас радует?

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

> чёт я там не увидел ничего уникального и нужного одновременного, может просмотрел?

ну как же !

unsigned int nick_l = strlen(nick), name_l = strlen(name);


и пусть там будут длинные строки - нам не лень два раза вызвать strlen при каждой вставке

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);



вообще красота - надо изменить значение в структуре на более длинное, что мы делаем - правильно, делаем невалидным указатель на структуру через realloc, заодно что делаем? правильно - копируем структуру и все-все данные и строки в ней

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

> во-первых речь шла о std::string

это один и тот же шаблон по сути

до недавнего времени у std::wstring имелись серьёзные косяки в реализации (в win wchar_t - 2 байта, в lin - 4, как хочешь так и ешь)


везде UTF32, а в виндовс UTF16

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

> 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);

вообще красота - надо изменить значение в структуре на более длинное, что мы делаем - правильно, делаем невалидным указатель на структуру через realloc, заодно что делаем? правильно - копируем структуру и все-все данные и строки в ней

эм, можно я не буду громко ржать? :)

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

во-первых речь шла о std::string

это один и тот же шаблон по сути

один то один, но параметры там разные

> до недавнего времени у std::wstring имелись серьёзные косяки в реализации (в win wchar_t - 2 байта, в lin - 4, как хочешь так и ешь)

везде UTF32, а в виндовс UTF16

сейчас уже не так (в win таки запилили wchar32_t), но осадочек остался :)

shty ★★★★★
()
Ответ на: комментарий от namezys
#include <QString>
#include <QMap>
#include <stdlib.h>
#include <iostream>


struct Person {
        QString name;
        int number;

        Person(const QString& a_name, int a_number) :
                name(a_name), number(a_number) {}
};

typedef QMap<QString, Person*> Persons;
typedef QMap<int, Person*> Numbers;

class Deleter {
        Persons& m_persons;
public:
        Deleter(Persons& a_persons) :
                m_persons(a_persons) {}
        ~Deleter() 
        {
                qDeleteAll(m_persons);
                m_persons.clear();
        }
};

int main(int argc, char* argv[])
{
        const QString nameTemplate = argv[1];
        int count = atoi(argv[2]);

        Persons persons;
        Numbers numbers;
        Deleter deleter(persons);

        for(int i = 0; i < count; i++) {
                QString name = nameTemplate + QString::number(i);
                Person* p = new Person(name, i);
                persons.insert(p->name, p);
                numbers.insert(p->number, p);
        }

        int ret = 0;

        for(Persons::const_iterator it = persons.constBegin(); it != persons.constEnd(); ++it) {
                ++ret;
        }

        return ret;
}
namezys@namezys-desktop:~/test_map/qt$ time ./qt name 1000000

real    0m2.623s
user    0m2.520s
sys     0m0.070s

давайте начнем с этого

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

> use case пожалуйста

чего? я копирую одну строку - получаю две. то есть побайтовое копирование. copy-on-write дешевле

это Вы про что?

Можно каждый раз не выделать память из кучи при работе со строками - использовать более быструю аллокацию

интерфейс какой библиотеки Вас радует?

boost'а больше. более С++ way. Критики stl среди С++ много

Хотя это лирика и стремление к совершенству. Работать с ней даже очень ничего.

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