Pull to refresh
39
0
Виктор Щерба @vScherba

Разработчик C++

Send message
Ну зачем же так голословно теоретизировать. Ну не поленитесь замерить производительность. Вот мой код для тестов:

Скрытый текст
#include <iostream>
#include <functional>
#include <map>
#include <chrono>

template<typename T> class Compare
{
public:
	enum Type { less, greater };
	Compare() = delete;
	Compare(Type type) : m_type(type) {};
	bool operator()(const T &_Left, const T &_Right) const
	{
		if (m_type == less)
			return (_Left < _Right);
		else
			return (_Left > _Right);
	};
private:
	Type m_type;
};

inline bool less(int a, int b)
{
	return a < b;
}

inline bool greater(int a, int b)
{
	return a > b;
}

int main()
{
	/*auto mg = std::map<int, int, std::function<bool(int, int)>>(
		std::greater<int>{}
		);
	auto ml =
		std::map<int, int, std::function<bool(int, int)>>(
			std::less<int>{}
		);*/
	
	/*auto mg = std::map<int, int, Compare<int>>(
		Compare<int>(Compare<int>::greater)
		);

	auto ml = std::map<int, int, Compare<int>>(
		Compare<int>(Compare<int>::less)
		);*/

	auto mg = std::map<int, int, bool(&)(int, int)>(
		greater
		);

	auto ml = std::map<int, int, bool(&)(int, int)>(
		less
		);

	for (int i = 0; i < 100000; ++i)
	{
		mg.insert({ i, i });
		ml.insert({ i, i });
	}

	const auto mm = { mg, ml };


	auto start = std::chrono::system_clock::now();
	int res = 0;
	for (const auto & m : mm)
	{
		for (int i = 0; i < 100000000; ++i)
		{
			res += m.at(i % 100000);
		}
	}
	std::cout << std::chrono::duration<double>(std::chrono::system_clock::now() - start).count();

	std::cout << res << std::endl;
}



На моем компьютере на Visual Studio 2015 результаты такие:
std::function — 24 сек
указатель на функцию — 17 сек
авторский Compare — 14 сек.

На GCC 6.3.0 на coliru.stacked-crooked.com аналогичный результат (понятно, что там нельзя мерить производительность, но под рукой нет GCC):

std::function — 29 сек
указатель на функцию — 22 сек
авторский Compare — 18 сек.

Если не сложно, проверьте, пожалуйста, на своем компиляторе, интересно сравнить.

Ничего против std::function не имею, сам люблю ее использовать, где это уместно, еще со времен буста.

То, что std::function оптимизирована с помощью SBO, никто ж не спорит, это прописная истина еще со времен проживания в Boost. Не хватало, чтобы она еще кучу мусолила. И тем не менее, косвенного вызова функции через указатель в std::function не избежать, увы, во время компиляции не известно, что спрятано внутри нее.
Код плюсую, но что-то в последнее время стало модно размахивать std::function, где уместно и где нет.
Все-таки компаратор в больших (а лучше во всех) STL-коллекциях должен инлайниться. Полиморфный же std::function не инлайнится и ударит по производительности кода и оптимизациям компилятора. Компаратор — это критический по производительности участок кода. Здесь лучше оставить авторский Compare.
В том-то и вся соль. Сейчас долги кого-то перед вами учитываются в ваших балансах как активы.
Вы взяли в одном банке 100 рублей и положили на депозит в другом (дали банку в долг), значит у вас есть 100 рублей как обязательство второго банка вам их вернуть.
В итоге, первый банк уже рисует у себя на балансе проценты, которые вы будете платить за кредит, вы получаете справку от второго банка, что у вас есть вклад на 100 рублей, и вы уже не нищий. Для налоговой, например или для взыскания алиментов у вас есть актив, а не 0 (а про долги никто и не спрашивал).

Если взаимно погасить, то у всех будет по нулям. А так, Петя, например, может продать Васин долг или заложить его Оксане, взяв у нее еще 75 рублей. Или может пиариться, что обладает активами в 100 рублей.
Вот так крутится финансовая система. А если взаимно обнулить, то и ликвидность будет 0, и встанет все и сразу. Не далеки те времена, когда эта финансовая модель с треском грохнется.
Я пока не осилил всю статью, но с первого примера возник вопрос: а почему бы просто не использовать bind?

const bool exists = WithObject (objectId,
        std::bind ([] { return true; }),
        std::bind ([] { return false; }));

// вернет false
// в MS VS 2012 в машинном коде в релизе нулевой оверхед: "push 0" - если результат передать аргументом другой функции
std::bind([] { return false; })(0, 3.14, "foobar");
Прошу прощения, возможно, я что-то пропустил, но с февральской конференции в Москве видео так и не выложили (за исключением 2-х или 3-х докладов по-моему). В почту ничего пока не приходило.
но передать ее потом куда-то не используя шаблонов затруднительно.
// Какой должна быть сигнатура функции, принимающей такой аргумент?
someOtherFunction(Lambda);

void someOtherFunction(decltype(Lambda) lam);
Число Пи здесь указано недостаточно точно, но дело в том, что целочисленные константы в заголовочных файлах не требуют выделения памяти в отличие от остальных констант

Не требуют выделения памяти до тех пор, пока кто-то не возьмет адрес у такой константы:

const int pi_i = 3;
const int* n = &pi_i;
или
void foo(const int* n);
foo(&pi_i);

Компилятор может по возможности не выделять память под константу, а не обязан это делать всегда.
Следует дополнить, если кто-то вдруг не уловил:
.NET-овский енумератор в данном случае — это обертка над COM-енумератором.
foreach перед выполнением получает lib.Assets и запрашивает у него COM-енумератор через скрытый get__NewEnum и оборачивает в .NET-енуменатор, ссылка на саму коллекцию lib.Assets нигде в оригинальном коде не сохраняется. Енумератор живет до конца foreach, а ссылка на саму коллекцию пропадает сразу же перед входом в цикл. Если бы коллекция жила внутри lib, то все было бы ок, но, судя по всему (нужно убедиться в исходниках), проперти lib.Assets генерит новую коллекцию всякий раз, когда к нему обращаются.

P.S. COM-енумератор — полноценный COM-объект.
Так очевидно же. Вы этой локальной переменной держите ссылку на коллекцию (то, что должен был делать правильно написанный енумератор) и не позволяете сборщику ее релизить раньше времени.
Действительно, проблема в COM. Я несколько лет разрабатывал COM-серверы на C++. Всякий разработчик COM знает, что енумератор, запрашиваемый у COM-коллекции через get__NewEnum, пока жив сам, должен держать ссылку на итерируемую коллекцию с поднятым счетчиком (за исключением копирующих енумераторов, которые копируют элементы коллекции в себя). В майкрософтовском ATL, на базе которого часто реализуются COM и коллекции в том числе, это сделано автоматом:
… holds a COM reference on the collection object to ensure that the collection remains alive while there are outstanding enumerators

Судя по симптомам, в Вашем COM-сервере об этом скорее всего забыли. Без исходников на 100% гарантировать конечно нельзя.

точно такая же работа без GetEnumerator() то есть по индексу — к дисконнекту не приводит

Как раз, потому что в этом варианте не участвует кривонаписанный COM-енумератор.
var asset = lib.Assets[i]; получаем коллекцию и напрямую (без промежуточного енумератора) из нее берем элемент.

Кстати, в COM можно реализовать проперти так, что Assets возвращает коллекцию, а Assets(i) сразу нужный элемент без создания и заполнения коллекции. Не знаю, поддерживает ли такое клиент на C#. Но это уже другая песня.
Действительно, в разделе 5.7 (4). Замечательно. В таком случае, у автора все в порядке с выходом за пределы. В выражении &x + 1 нет UB.
Спасибо. Для меня это оказалось откровением. Действительно, стандарт позволяет при адресной арифметике выходить за пределы массива только на 1 элемент (но не разыменовывать!). Далее одного элемента — UB.
Век живи — век учись.
А как насчет такой конструкции, будет ли это считаться UB?
int x;
std::accumulate(&x, &x + 1);

Скорее всего, да. Про адресную арифметику с переполнением я в стандарте нашел сноску только насчет массивов. Но можно ли указатель на единичную переменную трактовать как массив из одного элемента?
Еще более плохая практика. Вероятность нарваться на границу защищенного сегмента памяти — просто.

Здесь у автора все в порядке. Ссылаться за пределы массива можно и даже нужно. Итераторы конца последовательности (end) этим и занимаются. Разыменовывать нельзя, да, а указывать — на здоровье.
int (*x)[7] объявляет указатель. Его размер на стеке равен размеру указателя для данной платформы и не зависит от того, на что он указывает.
sizeof(int (*)[7] ) == sizeof(int (*)[1000] )== sizeof(int*)
Допустим, я компилирую PE / elf модуль под другую ОС (кто сказал, что PE только для MS?), где адрес 0 — корректный.

Вы же указываете компилятору целевую ОС. Ну вот, компилятор знает, что в целевой ОС адрес 0 корректный, значит для null-представления он выберет что-то более подходящее.
Случай действительно синтетический. Если бы возникала такая потребность, то компиляторы реализовали бы опции переопределения null-значения.
Это мои размышления, я не претендую на истину :)
А Вам переопределять и не нужно. Ваш компилятор знает, под какую архитектуру + ОС он компилирует, отсюда, ему виднее, какие адреса можно использовать как заведомо невалидные. Например, в Windows в подсистеме виртуальной памяти хоть для x86, хоть для IA-64 по нулевому адресу гарантированно нельзя разместить объект. Компилятор это знает и использует в этом случае 0 для физического представления null указателя.
Повторюсь еще раз насчет макроса NULL. В C++ NULL можно определить только константным целочисленным нулем. Попробуйте сделать редефайн #define NULL 1. В строчке кода int* p = NULL получите ошибку компиляции. Не кастуются в C++ целые числа к указателям, исключение только для константного нуля интегрального типа. Это правила языка. Кстати, Страуструп против использования макроса NULL в C++, говорит, выбросите его и используйте 0 (хотя, для меня NULL удобнее).
Ага, значит, мы можем переопределить null на конец памяти?

Если Вы о макросе NULL, то его переопределение вряд ли чем-то поможет, а скорее навредит. Подозреваю, что везде он будет определен как ноль, с различными вариациями типов 0, ((void*)0), ((int)0).
Само же особое состояние null указателя переопределить никто не может, оно задается компилятором. Мы лишь можем присвоить это состояние через p = 0 или p = NULL и сравнить с тем же 0 или NULL.
Тут путаница с самим словом NULL. В том же Паскале используется nil, оно чуть-чуть более нейтрально.

Как раз NULL и был призван решить эту путаницу, чтобы «0» не маячил перед глазами и не давал ложного ощущения, что p=0 — это присваивание адреса нулевой ячейки, p = NULL уже яснее. NULL не значит zero, NULL значит пустой, как в SQL.
Кстати, я смотрю дискуссия разделилась на независимые ветки, и нашу ветку не читают :), товарищи в других ветках продолжают наезжать на стандарт, за то, что он якобы не позволяет размещать данные в нулевой ячейке, вот наглядный пример интерпретации, якобы p = 0 обязательно настраивает указатель на нулевую ячейку памяти.
А почему не значения null, которое могло бы быть переопределено?

Этого я не знаю. Предположу, что по тем же причинам, что и отсутствие типа bool в C. Для нового значения null, нужно было вводить новый тип данных. Наверное, встроенные типы языка C должны были соответствовать типам платформы. В C++ же добавили nullptr с типом nullptr_t. Опять же, переопределить его никто не даст, также никто не обязан знать его физическое представление.

Подытожу:
1. #define NULL лишь для удобства чтения, на нем нет смысла зацикливаться, его переопределение ничего хорошего не даст, уверен, что на всех платформах он определен как 0 (с типом void* или без него или еще с каким-либо типом, но все равно 0).
2. Каждая платформа имеет свой зарезервированный пустой указатель. О его реальном физическом представлении знает лишь компилятор.
По стандарту нулевой адрес может быть вполне корректным. Язык C (а также C++) вводят понятие null указателя. Это указатель с определенным зарезервированным значением, которое сигнализирует, что указатель не указывает никуда. Это значение не обязано представляться физическим нулем, может быть, что угодно, например, 0xffffffff. Но, т.к. это представление сильно зависит от платформы, устанавливать значение указателя в особое состояние null необходимо присваиванием нулевого значения целого интегрального типа. То есть int* p = 0; инициализирует разрядную сетку указателя значением 0xffffffff, и при этом p == 0 — истина, if (p) будет ложно.
И это не только в теории. Есть системы, где null указатель не представлен физическим нулем: c-faq.com/null/machexamp.html
Возможно, я не прав, тогда пусть меня поправят.
Я предлагаю вот таким способом разрешить спор. Если я правильно понял стандарт, я могу написать свой компилятор, который, следуя букве стандарта, имеет полное право на каждый оператор p->x (в том числе &p->x) «трогать память» под переменной x (считывать в регистр, в отладочных целях, например). Вопрос эффективности не волнует, зачем это делать — тоже не важно. Ведь задача стандарта — быть максимально абстрагированным и не делать предположений о реализации.
В таком случае мой компилятор будет соответствовать стандарту, а ((T*)nullptr)->x будет стабильно падать. Также на моем компиляторе будет стабильно падать пример из комментария выше. Следовательно, оба примера являются UB.
Осталось доказать, что мой компилятор соответствует стандарту, и я ничего не упустил.
1
23 ...

Information

Rating
Does not participate
Location
Дубна, Москва и Московская обл., Россия
Date of birth
Registered
Activity