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

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

>>Не забыл что C++ еще выделенную память в конце чистит? Удали все элементы из GList, увидешь что время выполнения сравняется.

QList тоже в деструкторе каждый элемент чистит или только собственную память? Иначе будет нечестно.

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

> И как же люди в опенсурсе-то пишут...

Пишут, прекрасно пишут те задачи для которых ООП не нужно. А где нужно ООП пишут на С++. А не придумываю как линуксфан, свой собственный для каждой задачи отдельный ООП велосипед.

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

> QList тоже в деструкторе каждый элемент чистит или только собственную память? Иначе будет нечестно.

Он ее чистит честно - дергает деструктор. Можно сделать пометку для этого типа, чтоб удалял просто память. Еще быстрее будет

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

>>Он ее чистит честно - дергает деструктор. Можно сделать пометку для этого типа, чтоб удалял просто память. Еще быстрее будет

Он дергает СВОЙ деструктор, но не деструктор каждого элемента. Не?

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

> Но чтобы быть честным я ещё объявлю типы и буду использовать append:

ради интереса - прогоните на время плз мой вариант на С++ для общей картины

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

> Он дергает СВОЙ деструктор, но не деструктор каждого элемента. Не?

каждого элемента

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

>> Сформулируешь кратенько отличие Ъ-Си++ от «Си с классами»?

Я каждому местному троллю обязан восполнять пробелы в образовании?

А ты попробуй объяснить. Я думаю это доставит массу лулзов публике, а как известно смех удлиняет жизнь. Или ты эгоист и не хочешь сделать хорошо коммюните?)

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

> Он дергает СВОЙ деструктор, но не деструктор каждого элемента. Не?

Он дергает и деструктор каждого элемента, если не указано, что это QtTrivialType

namezys ★★★★
()

Я не понимаю, о чем тут спор, если честно.
Потому что у Си и C++ совершенно разные области применения, их не совсем корректно так вот сравнивать.
У Си область применения - низкоуровневое системное программирование. У C++ ее вообще нет.

Когда дело доходит до чего-то, отличного от байтоковыряния, когда используются «сложные структуры данных», значит надо выбрасывать Си, но это не значит, что надо использовать C++ - его вообще лучше не использовать, надо использовать нормальный высокоуровневый язык, со сборкой мусора и со вменяемым уровнем абстракции. Это еще Линус надвое сказал(http://www.realworldtech.com/beta/forums/index.cfm?action=detail&id=110618&th... последний абзац).

[а дебильный плюсовский подход к хранению объектов в коллекциях и составных структурах «in-place» меня всегда поражал - декларируется такая якобы экономия в плане свободного места и производительности(не надо разыменовывать указатели, радость то какая!), но совсем не учитывается громадный оверхед на постоянное копирование объектов туда-сюда]

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

> для других языков это не так

c Love5an не смысла спорить, у него по определению С++ говно, а CL няшка

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

> У Си область применения - низкоуровневое системное программирование. У C++ ее вообще нет.

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

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

> Я сейчас сделаю их такими, чтоб не появлялось констант времени компиляции и прогоню

кстати, если добавить "-static" то время выполнения моего примера можно сократить с .007 до 0.006 сек, а если добавить abort() - чтоб не очищать память( как у остальных примеров ), то 0.004 сек

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

Значит, если взять 1000.000 повторений, то вот так:

1) Вариант C + GLIB (мужика который)

real	0m0.444s
user	0m0.349s
sys	0m0.055s

2) C++ std:: *** (ахонимуса)

real	0m0.276s
user	0m0.193s
sys	0m0.016s

3) На CL

Evaluation took:
  0.212 seconds of real time
  0.185972 seconds of total run time (0.161975 user, 0.023997 system)
  [ Run times consist of 0.127 seconds GC time, and 0.059 seconds non-GC time. ]
  87.74% CPU
  538,488,519 processor cycles
  24,007,904 bytes consed

Ещё я заметил, что на C или CL можно неограниченно увеличивать размер списка, а вот на C++ после 10.000.000 начались вылеты.

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

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

Крутой низкоуровневый C++ наконец обзавелся стабильным ABI или до сих пор у каждого производителя компилятора свое мнение на этот счет?

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

а если учесть что она ваще ничего нужного неделает - то ее время выполнения равно 0лю - потому как ее запускать ваше ненадо
так ? :)

нагородили кучу примеров и нипонятно чего непонятно с чем сравниваете

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

но это ведь не чистое время - тут нет очистки списка, да?

Тут всё, включая расход на вызов и возвращение из функции (и подготовка списка, но списки там не такие просты - нельзя сказать, что они очищаются)

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

для других языков это не так

Для каких языков-то?

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

> но списки там не такие просты - нельзя сказать, что они очищаются

т.е. не очищаются, как я понял, получается что сравниваются разные задачи - С++ уже отдал всю память, а CL нет

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

> эта тема ничего и никаму недоказывает - сдаеться мне тема была специально сделана для флуда

я прав ?


Отнюдь. Я правда хотел понять, предоставляет ли язык C возможность оптимизации кода. В итоге я понял, что оптимизации достигаются с помощью копирования простыней кода с заменой типой и подстраиванием алгоритмов. Что само по себе рушит ту простоту C, за которую его так любят. А вот эту задачку так никто из сишников и не решил: http://www.linux.org.ru/forum/development/5154097/page8#comment-5156947

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

> Крутой низкоуровневый C++ наконец обзавелся стабильным ABI или до сих пор у каждого производителя компилятора свое мнение на этот счет?

Речь изначально шла не про ABI, а про то, какой код компилятор положит в обьектник.

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

С++ уже отдал всю память, а CL нет

Это тонкий вопрос :) т.к. память отдаёт GC. Но я замечу такую штуку - если говорить о реализации _одинаковых_ алгоритмов (одинаково хорошо), то среднестатистически можно разогнать CL до (иногда лучше) уровня Си, хотя в большинстве случаев это конечно не так - CL будет отставать. Собственно, бенчмарки на шутауте это и показывают (там есть парочка тестов где CL сравним с Си). Оффтопик, однако ;)

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

> но я замечу такую штуку - если говорить о реализации _одинаковых_ алгоритмов (одинаково хорошо), то среднестатистически можно разогнать CL до (иногда лучше) уровня Си

и ваш пример это тоже показал - реализация на CL оказалась быстрее реализации на glib

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

Чистый CPP:

#include <string> 
#include <list> 
#include <stdlib.h>
 
struct person  
{  
    std::string nick;  
    int id;  
};  
  
  
int main(int argc, char *argv[])  
{  
    std::list<person> list;
    int count = atoi(argv[2]);    
    list.resize(count); 
     
    const std::string str = argv[1];  

    std::list<person>::iterator it = list.begin(); 
    for( int i = 0 ; i < count ; ++i, ++it ) { 
       it->nick = str; 
       it->id   = i; 
    } 
}

namezys@namezys-desktop:~/test_qt/cpp$ time ./a.out dendy 10000000

real    0m1.319s
user    0m0.850s
sys     0m0.380s
namezys@namezys-desktop:~/test_qt/cpp$ time ./a.out dendy 10000000

real    0m1.358s
user    0m0.980s
sys     0m0.300s
namezys@namezys-desktop:~/test_qt/cpp$ time ./a.out dendy 10000000

real    0m1.353s
user    0m0.850s
sys     0m0.380s
namezys@namezys-desktop:~/test_qt/cpp$ time ./a.out dendy 10000000

real    0m1.368s
user    0m0.940s
sys     0m0.270s
namezys@namezys-desktop:~/test_qt/cpp$ time ./a.out dendy 10000000

real    0m1.320s
user    0m0.880s
sys     0m0.430s

но этот код с «шоткатом»

CPP + Qt:

#include <QString>
#include <QLinkedList>
#include <stdlib.h>

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[]) 
{
    Q_UNUSED(argc);
    Q_UNUSED(argv); 
    QLinkedList<person> list; 

    const QString dendy = argv[1];
    int count = atoi(argv[2]); 
 
    for (int i = 0; i < count; ++i) { 
        list.append( person(dendy, i) ); 
    } 
}

namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.586s
user    0m1.280s
sys     0m0.270s
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.584s
user    0m1.220s
sys     0m0.240s
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.616s
user    0m1.220s
sys     0m0.350s
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000
^[[A
real    0m1.728s
user    0m1.350s
sys     0m0.330s
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.571s
user    0m1.180s
sys     0m0.320s

Влияние QList и QLinkedList на глаз не заметил

Ну и теперь glib, чуть чуть поправленный мною. Я отказываюсь от того, что строки копируются, фактически получаю один объект, и все указывают на него. Тогда быстрее

#include <glib.h> 

typedef struct { 
   gchar* nick; 
   gint id; 
} person; 
 
int main(int argc, char** argv) 
{ 
   GList *list = NULL; 
   guint i; 
   gchar* dendy = g_strdup(argv[1]);
   int count = atoi(argv[2]);
 
   for (i = 0; i < count; ++i) { 
      person *Prs = g_slice_new (person); 
      Prs->nick = dendy; 
      Prs->id = i; 
      list = g_list_prepend (list, Prs); 
   } 
 
   //list = g_list_reverse (list); 
   g_list_free(list);
 
   return 0; 
}
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m1.548s
user    0m1.190s
sys     0m0.330s
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m1.472s
user    0m1.080s
sys     0m0.380s
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m1.496s
user    0m1.200s
sys     0m0.270s
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m1.472s
user    0m0.940s
sys     0m0.460s
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m1.407s
user    0m0.930s
sys     0m0.320s

А теперь варинт с получением независимых строк

namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m2.390s
user    0m1.710s
sys     0m0.440s
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m2.358s
user    0m1.770s
sys     0m0.550s
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m2.294s
user    0m1.680s
sys     0m0.560s
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m2.385s
user    0m1.810s
sys     0m0.500s
namezys@namezys-desktop:~/test_qt/c$ time ./a.out dendy 10000000

real    0m2.400s
user    0m1.730s
sys     0m0.510s

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

> а оптимизация есть - только ты неишеш оптимизацию - ты ищещ в точности тоже самое что и у тебя щас

Если бы я искал в точности такое же - я бы забраковал решение Мужика со связным списком, потому как в контейнер он ложит голую C-строку, у которой даже нельзя получить длину и создать копию без глубокого копирования. То-есть он написал ровно столько, сколько нужно было для замера времени.

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

>Речь изначально шла не про ABI, а про то, какой код компилятор положит в обьектник.

Какая к черту разница, какой код генерирует компилятор, если нет единого ABI? Возьмем icpc в те дремучие времена, когда он смотрел на плюсовое ABI иначе, нежели g++: какой прок от того, что он скомпилирует ClanLib более эффективно, если clanbomber все равно не соберется?

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

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

1. Ложат только в штаны. В остальные места кладут.

2. Мужик кладет копию строки, а строка, как завывали плюсофилы выше, — это последовательность байт, завершаемая нулем.

То-есть он написал ровно столько, сколько нужно было для замера времени.

Если не считать того, что он слукавил с освобождением, в остальном он не соврал.

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

что значит «итерироваться по всему списку» ?

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

В связи в оптимизаций без создания раздельных объектов привожу аналог кода на qt

#include <QString>
#include <QLinkedList>
#include <stdlib.h>

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

    QStringRef nick; 
    int id; 
}; 
 
int main(int argc, char *argv[]) 
{
    Q_UNUSED(argc);
    Q_UNUSED(argv); 
    QLinkedList<person> list; 

    const QString dendy = argv[1];
    int count = atoi(argv[2]); 
 
    for (int i = 0; i < count; ++i) { 
        list.append( person(dendy, i) ); 
    } 
}
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.029s
user    0m0.560s
sys     0m0.310s
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.103s
user    0m0.720s
sys     0m0.320s
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.104s
user    0m0.760s
sys     0m0.200s
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.055s
user    0m0.720s
sys     0m0.300s
namezys@namezys-desktop:~/test_qt/qt$ time ./qt dendy 10000000

real    0m1.050s
user    0m0.690s
sys     0m0.290s
namezys ★★★★
()
Ответ на: комментарий от linuxfan

> Какая к черту разница, какой код генерирует компилятор, если нет единого ABI?

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

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

Вообще проблема совместимости ABi - это не проблема компиляторов как таковых. А проблема отсутствия стандартна - зато есть хорошая гибкость

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

Вот вам точные тесты под разными углами с освобождением памяти (ну за исключением того, что можно было бы еще слукавить, и в варианте qt для ref объявить тип тривиальным и не требующим деструктурирования)

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

> Если не считать того, что он слукавил с освобождением, в остальном он не соврал.

Всё зависит от условия задачи... Стойте, так ведь не было никакого условия, Мужик просто написал что ему было удобно. А задача стояла так: http://www.linux.org.ru/jump-message.jsp?msgid=5154097&cid=5156974

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

>>голую C-строку, у которой даже нельзя получить длину и создать копию без глубокого копирования.

Qt головного мозга? Я не просто туда положил строку, а делал ее полную копию.

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

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

> Вот вам точные тесты под разными углами с освобождением памяти

нет смысла сравнивать создание 1000000 указателей на одну строку, так и в С++ можно было бы написать: list.resize( 10000000, &str );

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

>>Всё зависит от условия задачи... Стойте, так ведь не было никакого условия, Мужик просто написал что ему было удобно. А задача стояла так: http://www.linux.org.ru/jump-message.jsp?msgid=5154097&cid=5156974

Я написал ровно то, что занимало минимум времени, а делать за тебя лабы я не собираюсь за бесплатно.

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

> 2. Мужик кладет копию строки, а строка, как завывали плюсофилы выше, — это последовательность байт, завершаемая нулем.

о-О! Прогресс! Да вас уже дошло уважаемый, что в плюсах строки хранятся так же как и в Си ))) Не прошло и каких-то семь часов. А то писал раньше

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

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

> но стек-то не резиновый, так-что вариант с аллокацией на стеке...

О каком стеке вообще речь?

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