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

★★★★★

мы, дремучии C-шники делаем примерно так : include/linux/list.h и не полагаемся что всякие автоматические деструкторы, операторы присваивания и копирования сделают что-то-там-но-вроде-как-должно.

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

Да, конечно же, вы абсолютно правы, я погорячился со словами

С точки зрения производительности скорость работы при возврате ссылок и значений абсолютно одинакова.


Естественно манипулирование ссылками ликвидирует накладные расходы на инкремент/декремент. Имелось в виду, что это самая оптимальная альтернатива глубокому копированию. В нашей же задаче вычисление хеша и поиск по нему съест параллельные накладные расходы на инкремент/декремент. К тому же примеры не совсем идентичны, в случае со ссылкой код получается небезопастным, потому как после addPerson() возвращённая ссылка окажется невалидной, и искать почему рухнула программа можно будет ой как долго.

Но речь не об этом. Хотелось бы всё же услышать сишников.

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

> мы, дремучии C-шники делаем примерно так : include/linux/list.h и не полагаемся что всякие автоматические деструкторы, операторы присваивания и копирования сделают что-то-там-но-вроде-как-должно.

Другими словами, на вопрос

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


Ответ «Да»?

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

> Хотелось бы всё же услышать сишников.

один только-что забежал, сказал что-то невнятное и убежал :) но тут в принципе и слушать нечего - стандартного( основанного на стандартных библиотеках ) решения у них нет, разве что использовать qsort + bsearch, но это будет намного более нечитабельно чем С++-й вариант, ну или таскать на всех платформах за собой далеко не маленький glib

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

> ну или таскать на всех платформах за собой далеко не маленький glib

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

Dendy ★★★★★
() автор топика
Ответ на: комментарий от Dendy
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LEN 64
#define STRING_HASH 10

struct string{
	struct string *next,*prev;
	char nick[MAX_LEN];
	char name[MAX_LEN];
	};

struct strings{
	struct string *str[STRING_HASH];
	};

struct strings *string_init(){
	struct strings *s;
	s=malloc(sizeof(struct strings));
	memset(s->str,0,sizeof(void*)*STRING_HASH);
	return s;
	};

int add_str(struct strings *str, char *nick, char *name){
	struct string *s,*s2;
	char h;
	s=malloc(sizeof(struct string));
	strncpy(s->nick,nick,MAX_LEN);
	strncpy(s->name,name,MAX_LEN);
	h=nick[0]%STRING_HASH;
	if(str->str[h]==NULL){
		str->str[h]=s;
		s->next=s; s->prev=s;
		}
	else{
		s->next=str->str[h]; s->prev=str->str[h]->prev;
		str->str[h]->prev->next=s; 
		str->str[h]->prev=s;
		}
	}

struct string *find_str(struct strings *str, char *nick){
	char h;
	struct string *s;
	h=nick[0]%STRING_HASH;
	if(str->str[h]==NULL)
		return NULL;
	else{
		s=str->str[h];
		do {
			printf("%d %d\n",s,str->str[h]);
			if(strncmp(nick,s->nick,MAX_LEN)==0)
				return s;
			s=s->next;
			printf("%d %d\n",s,str->str[h]);
			}while(s!=str->str[h]);
		}
	return NULL;
	}

del_str(struct strings *str, char *nick){
	struct string *s;
	char h;
	h=nick[0]%STRING_HASH;
	s=find_str(str,nick);
	if(s==NULL)
		return;
	if(str->str[h]==s){
		if(s->next==s)
			str->str[h]=NULL;
		else
			s->next->prev=s->prev;
			s->prev->next=s->next;
			str->str[h]=s->next;
		}
	else{
			s->next->prev=s->prev;
			s->prev->next=s->next;
		}
	free(s);
	}

destroy_str(struct strings *str){
	struct string *s;
	int i;
	for(i=0;i<STRING_HASH;i++){
		if(str->str[i]==NULL)
			continue;
		s=str->str[i];
		do {
			s=s->next;
			free(s->prev);
			}while(s!=str->str[i]);
		}
	free(str);
	}

бугзы инклуде чтото типа хеша присутствуют ;) несомневаюсь что и сами могли такое придумать

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

почему вы думаете что маллок из кучи будет занимать большое время ? имхо нет

у меня имхо тако ощущение - что вы спрашиваете изза того что вам неизвестно как на с++ в либах это все организовано - и у вас сомнения насчет скорости и оптимальности

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

> Ответ «Да»?

а то, что за приведенным вами кодом на С++ с использованием Qt классов, скрывается куча вызовов методов самого Qt вас совершенно не волнует и вы смело говорите о производительности?

если у вас словосочетания «выделить память», «разыменование указателя», «тайп каст» и иже с ними вызывают рвоту и понос, то причем тут С?

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

> внимательно смотрим - и находим юзанье хеша

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

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


ну так об этом и речь

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

The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automat‐
ically freed when the function that called alloca() returns to its caller.

нуда - в томже самом стеке - который освободиться когда выйдет из функции

а нужно то было вроде как более менее постоянное выделение памяти

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

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

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

ну да, ну да, и вертеть потом, на стеке, в рамках main() видимо, «сложными структурами». прослезился даже

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

а вот в том то ли речь или не в том - это уже вопрос к постановщику
это неизвестно

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

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

> ну да, ну да, и вертеть потом, на стеке, в рамках main() видимо, «сложными структурами». прослезился даже

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

ahonimous
()

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

С библиотек, дла решения перечисленных ОПом задач, не встречал

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

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

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

о госспади, а где выделять? на стеке?


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

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

ну а шо вы хотели - это С ! ;) почти асм

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

а отсутвие библиотеки - означет что эту библиотеку и неискали
а библиотека на Си - будет означать кучу функций
а более обьеденненная система будет означать целую систему - чем посуществу и являеться С++
вопрос в том - нужно ли это

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

> а вот в том то ли речь или не в том - это уже вопрос к постановщику

это неизвестно

все остальное - будет требовать ресурсов при других операциях



поэтому проще использовать map, зная, что в случае чего можно поменять только в одной строке универсальный map на hash_map

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

> универсальность и оптимизация это несовместимы

еще как совместимы, вот смотрите - вы написали один вариант хэша, а теперь выяснилось, что лучше искать сначала по идентификатору из той же структуры + имя, что сделает программист на С++? да просто перепишет функцию для hash_map, она будет inline и прекрасно впишется в шаблон без всяких потерь в производительности и необходимости менять что-либо, кроме одной строки в старом коде, а вам придется пройтись по каждой функции, что вы написали

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

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

при чем здесь шаблоны и что такое «безопасность внешнего интерфейса»?

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

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

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

Dendy суть в том - что вы ставите С++ задачу - и простите ее решить на С - приэтом оперируя всеми темиже С++ критериями
тут язык другой - тут по другому надо общию задачу ставить

поставте общию задачу тогда а

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

> меняете супер пупер хеш функцию nick[0]%10 на define и прописываете туда чего угодно - хоть md5 всей строки

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

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

да и макросов таки придется использовать побольше - ведь кроме хэша есть еще тип ключа и значения

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

удаление есть
очистки - чего очистки то ?
диапазона ? а мож сразу ИИ прикрутим и пускай деньги зарабатывает ? :)

вот чем это не мелкая бибилиотека
связанные списки + поле размера структуры - уже годиться для завертывания чего угодно :)

ae1234 ★★
()

Или может GTK неудачный пример

Да

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

вот чем это не мелкая бибилиотека
связанные списки + поле размера структуры - уже годиться для завертывания чего угодно :)

ну вот на С++ можно написать так:

hash_map<string,Person,hash<string>> items;
items[ "1111" ] = { "DDDD", 66 };

а как будет выглядеть использование этой библиотеки?

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

вы всерьез думаете я понимаю что вы написали ? :)
я ваще незнаю С++

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

ae1234 ★★
()


Для примера я взял односвязные списки в GTK GSList и Qt QList

с чего вы решили что QList это односвязный список?

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

дословно процитирцую: Internally, QList<T> is represented as an array of pointers to items of type T.
кто то что там про бессмысленное использование памяти написал..

Это даже не затрагивая вопрос о типизации, в C++ компилятор сразу даст по рукам

при попытке записать в контейнер неверный тип или

вы только попросите, вам здесь приведут кучу примеров на c++, и компилятор даже не ругнется


QString foo()

{

return listOfStrings.at( 5 );

}



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

поскольку время тратится только на увеличение счётчика и копирование указателя.

только до того момента пока не будем изменять данные, да? тогда аналог в С тот же самый указатель - const char*

для int будет вызван memcpy()

все так плохо?

Наоборот - всё хорошо. При удалении значения из середины списка остальные значения

подвинутся с помощью простого memcpy() без цикла с оператором копирования.

Чего хорошего делать memcpy, когда можно обойтись swap + decrement size

Собственно, мне интересно каков принцип работы сишников в таких простых задачах.

Наплевать на производительность, выделять всё в куче, делать глубокое копирование сложных структур

ага, это qstring то сложная структура

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

ага, давайте нам sizeof(QString)

И еще, если в Qt большая часть контейнеров implicitly shared не нужно заблуждаться, игнорировать ссылки и думать что это везде в C++ это так, а то привыкните и больно будет.

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

> такчто для начала разьясните чего вы написали

есть структура

struct Person
{
string name;
char age;
};

у Person есть ник, в С++ мы его не будем хранить в самой структуре - это избыточно, вы же можете - если вам удобно, так вот - нужно завести список из Person и иметь возможность:

а) добавить новые элементы и удалить старые
б) производить поиск по нику
в) указать свою функцию для хэша + опционально иметь набор предопределенных функций для стандартных типов

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

> при чем здесь шаблоны

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

и что такое «безопасность внешнего интерфейса»?


Хотя бы то, что нельзя перепутать добавляемый в контейнер экземпляр (скопипастить и вместо double подставить int, после чего долго сидеть в отладчике из-за race condition), нельзя перепутать аллокатор, компилятор тут же даст по рукам.

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


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

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

> поставте общию задачу тогда а

Общая задача - хранить список с ником и именем и по нику находить имя в этом списке.

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

struct Person
{
string name;
char age;
};

ну это запихиваем в мою супер либу заместо name - и все
дополнительно присабачиваем в структуру string поле размера - и незабываем учитывать реальный размер структуры
а хеш функцию - оформляем как реальну функцию - которой преедаюеться указатель на данные которыенадо прохешить

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

> ну это запихиваем в мою супер либу заместо name - и все

т.е.? в С++ ничего нигде менять не надо - эти две строки уже рабочие, а вы предлагаете руками для каждого случая копипастить новые варианты?

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

> Dendy а макс размер строки - то просто для убирание фрагментации памяти :)

Ловлю на слове. Если это действительно «просто», то напишите для строк с произвольным размером.

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

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

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

Dendy
ну MAX_LEN вместо четкозаданной заменить на передаваемую в функцию переменную - и учитывать ее в структуре
и strcpy заменить на memcpy
чего сложного

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

> с чего вы решили что QList это односвязный список?

Прошу прощения, просто список, совсем бессвязный (-:

дословно процитирцую: Internally, QList<T> is represented as an array of pointers to items of type T.


Опять ваша правда, это у меня каша в голове, я говорил про QVector.

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

вы только попросите, вам здесь приведут кучу примеров на c++, и компилятор даже не ругнется


Наделать ошибок можно на любом языке, не о том речь (-: Поэтому сразу и предложил не затрагивать этот вопрос.

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

только до того момента пока не будем изменять данные, да?


Там CoW, возвращается логическая копия, полностью безопастная и независимая от того, откуда она взялась.

Чего хорошего делать memcpy, когда можно обойтись swap + decrement size


QList и QVector сохраняют последовательность данных.

ага, давайте нам sizeof(QString)


sizeof(QString) == 4 на 32 битах и sizeof(QString) == 8 на 64 битах.

И еще, если в Qt большая часть контейнеров implicitly shared не нужно заблуждаться, игнорировать ссылки и думать что это везде в C++ это так, а то привыкните и больно будет.


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

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

в случае со ссылкой код получается небезопастным, потому как после

addPerson() возвращённая ссылка окажется невалидной,

и искать почему рухнула программа можно будет ой как долго.

бред, вы просто не в курсе что такое QList

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

> чего сложного

Я так понимаю MAX_LEN в структуре останется, а передаваемый размер строки обязан быть меньше него? Как-то неоптимально.

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

ага, давайте нам sizeof(QString)

sizeof(QString) == 4 на 32 битах и sizeof(QString) == 8 на 64 битах.


сейчас разберемся, давно не смотрел хидеры
если не затруднит, раз вы такй спец по Qt? дайте пожалуйста прямую ссылку на qstring.h версии 4.6 найти не могу.

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

сейчас разберемся, давно не смотрел хидеры

там только одно поле - указатель на структуру Data:

QBasicAtomic 	ref
int 	alloc
int 	size
ushort * 	data
ushort 	clean: 1
ushort 	simpletext: 1
ushort 	righttoleft: 1
ushort 	asciiCache: 1
ushort 	reserved: 12
ushort 	array [1]
ahonimous
()
Ответ на: комментарий от anonymous2

> бред, вы просто не в курсе что такое QList

Во-первых, речь там шла о QHash. А вообще внутренние данные могут быть скрыты и из интерфейса понять были ли они перемещены после мутабельной операции или нет не всегда возможно.

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