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

Я привел разные варианты: std::string фактически копирование строки (хоть там и хак есть), QString это copy-on-wrtie, QStringRef это как раз аналог работы с указателями, а для glib сделаны оба варианта - с копированием и без

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

>>Мужик. Ты мои тесты видел? Что скажешь?

Что-что, на разных способах выделения памяти работают примерно одинаково, если же полностью перейти на указатели - Qt сольет. Потому что как только я начинаю выделять память для GList через стек - Qt опять начинает сливать, причем почти в два раза.

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

Dendy, а в чём подвох? В том что сложность должна быть логарифмической (хэш)? Просто O(N) итератор как бы это раз плюнуть.

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

Чтоб обеспечить хотя бы приемлемую консистенстность данных

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

Да, разница всего-то в полтора раза на такой простой задаче. А что дальше будет?

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

> Мужик. Ты мои тесты видел? Что скажешь?

Присоединяюсь!
Народ вызывает светило на сцену для финального монолога )))

ЗЫ: Такой ответ «Проверил у обоих вариантов списки с указателями - Qt жестко слило.» без результатов тестов и их кодов не прокатит.

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

> Dendy, а в чём подвох?

Подвох в том, чтобы написать на C код, который хоть как-то вписывается в реальную задачу. А не померять скорость заполнения бесполезного списка.

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

ну давай пройдемся по всем, и у каждого третьего добавим к строке его номер. Только вот я не помню, как это делать на cpp, а уж тем более на glib

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

сложно смириться с тем фактом, что в C++ в большинстве случаев память освобождается автоматически?

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

Мужик. Ты мои тесты видел? Что скажешь?

На счёт тестов могу заметить, что QString конвертирует данные в UTF-16. Правильно было бы сравнивать пример Мужика с QByteArray.

QString, глубокое копирование:

dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.280s
user    0m0.220s
sys     0m0.056s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.282s
user    0m0.224s
sys     0m0.052s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.263s
user    0m0.204s
sys     0m0.056s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.272s
user    0m0.212s
sys     0m0.056s

QByteArray, глубокое копирование

dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.234s
user    0m0.192s
sys     0m0.040s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.245s
user    0m0.212s
sys     0m0.028s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.226s
user    0m0.172s
sys     0m0.052s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.229s
user    0m0.188s
sys     0m0.040s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.234s
user    0m0.212s
sys     0m0.024s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.228s
user    0m0.188s
sys     0m0.036s

Разница около 12%.

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

Списки сравнивать не очень интересно. Давай лучше сравним GTree и std::map. Вот где веселье то будет, потому что GTree будет на порядок медленее :)

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

Как раз для строк hash будет быстрее, по крайней мере в реализации unordered_map.

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

ЗЫ: Такой ответ «Проверил у обоих вариантов списки с указателями - Qt жестко слило.» без результатов тестов и их кодов не прокатит.

Договорились. С учетом всех пожеланий (очистка списка и юникодные строки), привожу свежий, честный тест.

Для Qt:

 const QString dendy = "Dendy";

struct person {
 person(const QString& a_nick, int a_id) :
 nick(a_nick), id(a_id) {}

QString nick;
 int id;
};

int main(int argc, char *argv[])
{
QLinkedList <person> list;

for (int i = 0; i < 1000000; ++i) {
 list.append (person (dendy, i));
}

}

Время:

 real   0m0.350s
 user   0m0.312s
 sys   0m0.032s 

Исправленный вариант теста для GLib:

 const gchar* dendy = "Dendy";

int main(int argc, char** argv) {
    GList *list = NULL;
    guint i;

   typedef struct {
       gint id; 
      gchar *str;
    } person;

   person *Prs;

   for (i = 0; i < 1000000; ++i) { 
      Prs = g_new (person, 1);
       Prs->id = i;
       Prs->str = g_strdup (dendy);
       list = g_list_prepend (list, Prs); 
   }

   list = g_list_reverse (list);

   g_list_foreach (list, (GFunc) g_free, NULL);
   g_free (list);

   return 0;
 } 

Время:

 real   0m0.277s
 user   0m0.252s
 sys   0m0.024s 

Как видишь, это я что-то ошибся с примерно равным временем (он будет таким, если не использовать строки вообще). Все те же 30% выигрыша в скорости у С.

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

О кого к нам занесло ... В прошлый раз жидко обкакался с std::map'ом и позорно свалил. Хочешь повторения макания в дерьмо?

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

В тесте выше я один раз определил QString, там постоянного конвертирования нет.

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

>>сложно смириться с тем фактом, что в C++ в большинстве случаев память освобождается автоматически?

Мне удобно самому освободить память тогда, когда потребуется.

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

>>Списки сравнивать не очень интересно. Давай лучше сравним GTree и std::map. Вот где веселье то будет, потому что GTree будет на порядок медленее :)

Теперь твоя очередь писать тесты :)

MuZHiK-2 ★★★★
()
Ответ на: комментарий от MuZHiK-2
namezys@namezys-desktop:~/test_qt$ time qt/qt 

real    0m0.166s
user    0m0.130s
sys     0m0.010s
namezys@namezys-desktop:~/test_qt$ time c/a.out 

real    0m0.273s
user    0m0.210s
sys     0m0.060s

namezys@namezys-desktop:~/test_qt$ gcc --version
gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3
Copyright (C) 2009 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
namezys ★★★★
()
Ответ на: комментарий от MuZHiK-2
#include <string>
#include <list>

using namespace std;

const string dendy = "Dendy";

struct person
{
        person (const string& a_nick, int a_id) :
                        nick (a_nick), id (a_id) {}

        string nick;
        int id;
};

int main (int argc, char *argv[])
{
        list <person> list;

        for (int i = 0; i < 1000000; ++i)
        {
                list.push_back (person (dendy, i) );
        }

}

Время Си

real 0.19
user 0.12
sys 0.07

Время Си++

real 0.08
user 0.05
sys 0.04

Си слил более чем в два раза.

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

Скопировал 1 в 1 ваш код:

glib:

dendy@linux-yfxp:~/tmp/container/glib> time ./glib

real    0m0.255s
user    0m0.188s
sys     0m0.064s
dendy@linux-yfxp:~/tmp/container/glib> time ./glib

real    0m0.271s
user    0m0.212s
sys     0m0.052s
dendy@linux-yfxp:~/tmp/container/glib> time ./glib

real    0m0.240s
user    0m0.176s
sys     0m0.060s

Qt:

dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.119s
user    0m0.092s
sys     0m0.024s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.120s
user    0m0.092s
sys     0m0.024s
dendy@linux-yfxp:~/tmp/container/qt> time ./qt

real    0m0.130s
user    0m0.100s
sys     0m0.032s

И да, где вы очищаете память, выделенную в strdup()?

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

Мужик видимо читерит и собирает код с -O0 :)

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

И да, где вы очищаете память, выделенную в strdup()?

ой, если мы и free поставим, то вообще жопа будет ...

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

cpp в режиме debug.

с в режиме -О2.

Код мужика

namezys@namezys-desktop:~/test_qt$ time qt/qt 

real    0m0.288s
user    0m0.210s
sys     0m0.030s
namezys@namezys-desktop:~/test_qt$ time c/a.out 

real    0m0.278s
user    0m0.200s
sys     0m0.050s
namezys ★★★★
()
Ответ на: комментарий от Reset

В прошлый раз? Обосрался? Это тогда-то, когда я утверждал, что map так назвать мог только объевшийся рыбы шизофреник?
Нет.
В прошлый раз я заебался об очевидных вещах с дебилами спорить.

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

> Что я делаю не так, как ты?

Мда, однако странно. Ждем ответного слова!

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

>map так назвать мог только объевшийся рыбы шизофреник

map по английски отображение. map в c++ это тоже отображение. Что тут неочевидного?

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

То, что обычно это называется словарь или хэш-табличка.

Map это очень эээ... необычно. Ну, в духе шизофренического stl, впрочем.

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

Просто идиоты создатели лиспа обозвали отображение словом dictionary (словарь) вот поэтому в Си++ называется «направильно», а в лиспе конечно же «правильно».

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

хеш-таблица это уже сведения о реализации. map это не хеш-таблица

Map это очень эээ... необычно.

для больных лиспом головного мозга конечно

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

> предожив называть map hash_map'ом

А что его так взбудоражило? Он думал там поэлементное сравнение, а оказался хэш )))

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

>dictionary (словарь)
Во-первых, в отличие от создателей C++, ни один создатель ни одного из диалектов лиспа, ну кроме, может быть, NewLISP, идиотом не являлся.
Во-вторых, в CL dictionary нет. Там есть списки и функция assoc. А еще есть хэш-таблички.

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

и да, создателей лиспа не так много, всего один же

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

В общем случае map не обязательно структура на основе бинарного дерева. Хотя в stl по стандарту это так.

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

map это ассоциативный массив, а hashtable это реализация ассоциативного массива. В c++ могли это назвать redblacktree, но зачем программисту знать детали реализации?

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

Для неумных и неэрудированный плюсовиков это вполне нормально, да.

А вот во всем цивилизованном мире называние ассоциативного массива именем всем известной функции, а не dictionary или [hash]table, у вменяемых людей вызывает когнитивный диссонанс.

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