Комментарии 76
они слишком абстрактны и нужны только для очень абстрактного кода
скорее «они очень абстрактны потому, что нужны для любого не очень абстрактного кода».
Моя личная претензия к с++20 (даже не к ranges как таковым) — отсутствие генераторов, с помощью которых уже можно было бы выразить и ranges, и корутины.
Antervis
Генераторы можно делать с помощью корутин, а можно делать и без корутин. А вот корутины с помощью генератора делать это я слабо себе представляю. И да в С++20 корутины же будут. В vs19 их можно включить кстати. Также корутины можно эмулировать по средством потоков или файберов.
Вот например
int main()
{
auto gen = generator(std::tuple<int, int, int>)
{
for (int z = 1; ; ++z)
for (int x = 1; x <= z; ++x)
for (int y = x; y <= z; ++y)
if (x*x + y*y == z*z)
co_yield(std::make_tuple(x, y, z));
};
for (int i = 0; i < 100 && (bool)gen; i++)
{
auto val = gen.next();
printf("(%i,%i,%i)\n", std::get<0>(val), std::get<1>(val), std::get<2>(val));
}
return 0;
}
cpp.sh/8dy27
Вы наверно хотели сказать что генератор это частный случай корутин.Ммм. Не стоит комментировать, едучи в автобусе, даже если там больше нечем особо заняться…
Выразался очень смутно, но имел в виду то же, что и вы. Собственно как пишет Wikipedia: Generators, also known as semicoroutines, are a special case of (and weaker than) coroutines, in that they always yield control back to the caller (when passing a value back), rather than specifying a coroutine to jump to
Но это у нормальных людей так. У Antervis генераторы явно не такие, как у нормальных людей, раз через них и ranges и корутины выражаются… потому и возник вопрос: а что он, собственно, под словом «геренатор» подразумевает?
Конечно та же Wikipedia рассказывает как корутины эмулировать на генераторах… но тут надо понимать, что эмулировать можно вообще всё на почти всём (главное вначале машину Тьюринга построить, а дальше задача сводится к предыдущей), вопрос же не в этом.
Вы наверно хотели сказать что генератор это частный случай корутин.генератор в частном случае может являться zero-cost абстракцией. Корутина таковой (в нынешней инкарнации) не является, и поэтому делать zero-cost генераторы через корутины не получится. Из-за этого нет смысла объединять интерфейсы корутин и ranges, чего, собственно, мне бы и хотелось. «Генератором» я и назвал такой объединенный интерфейс, который не обязан удовлетворять требованиям корутины, но позволяет писать код через yield/await.
Antervis
Генераторы можно делать с помощью корутин, а можно делать и без корутин
И да в С++20 корутины же будут. В vs19 их можно включить кстатии в gcc есть ветка с корутинами.
генератор в частном случае может являться zero-cost абстракцией.корутина в частном случае — тоже может. В общем — ни там, ни там нет.
На самом деле с короутинами история STL повторяется. Если посмотреть на существующую реализацию, то память уже не выделяется, но какие-то «следы» в коде остаются… думаю со времением и их изведут.
Корутина таковой (в нынешней инкарнации) не является, и поэтому делать zero-cost генераторы через корутины не получится.Почему нет? В языке это ничто не запрещает, а компиляторы, я думаю, подтянутся…
«Генератором» я и назвал такой объединенный интерфейс, который не обязан удовлетворять требованиям корутины, но позволяет писать код через yield/await.Осталось понять чем это от существующего предложения отличается…
и в gcc есть ветка с корутинами.А в clang не нужна ветка — там это опция компилятора.
корутина в частном случае — тоже может.теоретически язык вообще мало что запрещает, пока что только с std::unordered_* облом. А пока практической возможности нет, смысла тянуть это в язык тоже нет.
…
В языке это ничто не запрещает, а компиляторы, я думаю, подтянутся…
Если посмотреть на существующую реализацию, то память уже не выделяетсястрочка 5 асма — call operator new.
А пока практической возможности нет, смысла тянуть это в язык тоже нет.C++ так не работает. STL стал «zero-cost абстракцией» примерно лет через десять после того, как его в стандарт включили. Важна теоретическая возможность, а не то, что делают реальные компиляторы «здесь и сейчас». Если вам этот подход не нравится — вам нужно работать с каким-нибудь другим языком, C++ всегда так был устроен…
Смтрю в книгу — вижу фигу. А ничего что эта строчка при работе программы (то есть функииЕсли посмотреть на существующую реализацию, то память уже не выделяетсястрочка 5 асма — call operator new.
bar
в вашем случае) никогда не вызывается? От выделения памяти все компиляторы уже давно избавились, а вот оптимизации… да, «провисают» пока. Ну ничего — лет через 10 поправят.По крайней мере, в значительной части случаев это всё ведёт к более краткому, выразительному и понятному коду.К сожалению подавляющее большинство программистов не умеют в математику и для них функциональный подход выглядит сложнее, чем даже все эти короутины и ranges. Смиритесь.
Я тоже не понимаю почему — это просто такой факт, который я вижу на практике.
template <typename T>
double integrate(T generator) {
double acc = 0;
double t;
while(generator(t)) {
acc += dt_fixed * f(t);
}
acc -= 0.5 * dt_fixed * f(0);
acc -= 0.5 * dt_fixed * f(tau);
return acc;
}
long long int i = 0;
double res = integrate( [&i, n_nodes](double& t)
{
t = static_cast<double>(i);
++i;
return i < n_nodes;
} );
(Возможно где-то ошибся, но идея думаю ясна.)
Сам я рэнжи не смотрел, лишь слышал о плохой производительности, так что ничего об их (не)целесообразности сказать не могу. Но их нетривиальность с ходу вызывает опасения, что их начнут использовать не к месту, тем самым усложняя код.
Тоже хороший вариант, добавил его в репозиторий в виде v6.cpp. Выполняется около 4.5 с при компиляции и g++, и clang++.
Меня в нём смущает висячая long long i
, чтоб её убрать, надо делать генератор объектом класса (который хранит i), писать конструкторы и в итоге получится не сильно короче, чем с итераторами. Хотя этот вариант в целом попроще.
Мне кажется, не на пустом. Представьте, вам справку писать придётся по этому коду. И вы напишете "Вот так это нужно использовать… И не забываем оборачивать в {}!"? Не самый это изящный вариант. И функции нечистые я не люблю. А с итератором сразу понятно, что внутри грязная функция спряталась.
Отличная штука для того, чтобы собрать что-nj для себя и использовать, категорически недопустима для замеров скорости.
Потому что фиг его знает что именно у вас там в CPU есть — это ж не только от модели CPU может зависеть, некоторые фичи могут и от версии операционки или BIOS зависеть!
Как ни странно, но для моего Xeon-а — нет, не влияет. Только для clang++ v1 стал на 0.5 с быстрее с ним, для остальных всё осталось в пределах +- 0.1-0.2 с.
Будем интегрировать методом трапеций вот такую функцию:А какой в этом смысл, если эта функция прекрасно интегрируется аналитически? На что у меня потребовалось меньше времени, чем на прочтение следующего абзаца.
Конечно, тестировать производительность лучше всего на примере, для которого заранее известен ответ, иначе можно написать очень быстрый, но неправильный код. Аналитический ответ, кстати, есть в тексте статьи.
Статья не про численное интегрирование. И не про интегрирование вообще. Она даже не в хабе «Математика». Статья про новую фишку C++20.
Автор подобрал максимально простой пример, в котором эту фишку можно использовать, понятный большинству читателей. Вместо интегрирования методом трапеций тут могло быть вообще всё что угодно, использующее цикл с переменным шагом.
Статья бы только выиграла, будь в качестве примера хотя бы эллиптический интеграл.Статья бы, конечно, проиграла, если бы в качестве примера был взят эллиптический интеграл.
Потому что важно не то, что умеет автор стартьи (он же не для себя статью пишет!), важно что знают и умеют читатели.
Так вот про интегралы и как их считать — рассказывается даже в школьной программе, а вот эллиптических — там уже нет.
Если ваша задача рассказать про фишки C++, а не про математику кривых — то лучше, чтобы эллиптических интегралов в статье бы не было.
И ещё больше бы выиграла, если бы код из неё можно было бы взять и использовать «как есть» в реальных задачах.Возможно, но тут проблема курицы и яйца: пока ranges не в релизе — их мало кто использует, а когда будет набрана статистика использования — писать «вводную» статью типа обсуждаемой… уже поздновато.
Статья бы, конечно, проиграла, если бы в качестве примера был взят эллиптический интеграл.То есть, если бы автор начал статью не с «найти интеграл функции ...» а с «посчитать длину кривой» было бы хуже?
Потому что важно не то, что умеет автор статьи (он же не для себя статью пишет!), важно что знают и умеют читатели.Если статья о том, что все и так знают, то какой вообще смысл её читать?
Если статья о том, что все и так знают, то какой вообще смысл её читать?Естественно статья должна включать что-то, чего люди не знают. Но все «посторонние» вещи — желательно свести к минимуму.
То есть, если бы автор начал статью не с «найти интеграл функции ...» а с «посчитать длину кривой» было бы хуже?Да, потому что понять — откуда там вообще берутся проблемы гораздо сложнее.
Если статья о том, что все и так знают, то какой вообще смысл её читать?
Статья не об интегралах, а о ranges в C++20. Интегралы тут вообще сбоку, как пример.
Математику за то и не любят, что её дают на примерах, отдалённых от реальности; и многие вещи, звучащие страшно и непонятно, на деле оказываются простыми и очевидными — если их излагать не как «вешь в себе», а применительно к реальным задачам.
Математику за то и не любят, что её дают на примерах, отдалённых от реальностиЭто можно ставить в упрек статьям по математике, но никак не статьям в которых сама математика — пример использования чего-то.
З.Ы. Мое сообщение выше действительно стоило плевка в карму? Серьезно?
По содержанию. Изображая из себя знающего человека вы прокололись, назвав эллиптический интеграл «термином» — в то время как это никакой не термин, а функция. По основной теме статьи — ranges — вы также ничего сказать не смогли.
Вы никак не подтвердили свою квалификацию, чтобы позволить себе подобного рода высказывания. Я считаю, что вам и подобным вам нет места на Хабре. Ваше последующее прилюдное нытьё по поводу потери единицы кармы лишь подтвердило это.
Ну а если другим членам сообщества, минусующим мои комментарии, ваши более интересны — имеют на то полное право.
P.S. Чтобы читать о фичах языка без матана и прочей шелухи, нужно читать просто документацию.
Вы шли мимо, увидели, что кого-то сильно минусуют, и решили попинать его за компаниюЭто только в вашем видении. А в моем видении я прочел статью, спустился в комментарии, прочел их, и в ответ на утверждение с которым не согласен выразил свое мнение, без хамства и брани. Собственно, для этого и существуют комментарии.
Изображая из себя знающего человека вы прокололись, назвав эллиптический интеграл «термином»Вообще-то, под «термином» подразумевался «матан». Очень любят его поправлять, вот я и подстраховался.
прилюдное нытьё по поводу потери единицы кармыОбидно конечно, но меня возмутила совсем не потеря кармы, а то, в ответ на какой комментарий она последовала. Я бы понял, если бы там был переход на личности, превышение градуса, введение в заблуждение, или хотя бы напряженный спор между нами. Но там было весьма нейтрально выраженная одним коротеньким комментарием точка зрения по совершенно безобидной теме. Скорее я поверю что вы просто в отместку за минуса прошлись по всем несогласным не глядя, чем в мотивацию которую вы описали выше.
Лучше расскажите как в C++20 построить график этой функции и сохранить его pdf.
for(long long i = 1; i < n_fixed - 1; ++i) {
double t = dt_fixed * static_cast(i);
acc += dt_fixed * f(t);
}
Чего-то я туплю, но почему у вас этот подсчет называется методом трапеции?
Тем не менее в примерах мы почти до самого конца "забудем" про настоящий метод трапеций и для простоты будем рассматривать его версию с постоянным шагом, держа при этом в голове то, что сетка может быть произвольной.
Меня интересовал вопрос — как сетку в интегрирующую функцию передать, а не как именно написать метод трапеций для этой функции. По сути здесь речь об удобстве написания/поддержки без потери производительности. Честный метод трапеций, с переменным шагом там есть в конце (в репозитории — файл v5.cpp).
Мне всегда казалось что площать трапеции вычисляется как-то так:
s=(f(t0)+f(t1))*h/2
где t1=t0+delta_t
Это и написано в v5.cpp. Если же t_{i+1} — t_i = const (то есть не зависит от i), то из метода трапеций получается то, что написано в v1-v4. Просто формулу трапеций так можно преобразовать в случае постоянного шага. Вычислений при этом меньше, скорость — выше.
Хотя было бы, наверное, проще так и написать, чем пытаться «эзоповым языком» на этот вариант вывести.
Метод правых/левых прямоугольников менее точен, чем метод трапеций. Я же говорил про то, что википедия зовёт формулой Котеса.
Общности в таком подходе — хоть отбавляй, а что с производительностью? С потреблением памяти? Если раньше у нас всё суммировалось на процессоре, то теперь приходится сначала заполнить участок памяти, а потом читать из него. А общение с памятью — довольно медленная вещь.
Хм, ну подождите, если ставится задача «делаем универсальную функцию интегрирования трапециями, в которую снаружи передается набор узлов», то без трат памяти не обойтись. Кто-то где-то в другом месте этот набор генерирует и так или иначе записывает его в память, чтобы нам передать. Этот генератор может быть разным по устройству, не обязательно компилируемым вместе с функцией интегрирования, возможно набор узлов вообще по почте приходит.
К слову, если ваш второй пример запустить, то (количество узлов я взял поменьше, а то у меня чего-то bad alloc вылезал) результат таков: 1.3 секунды занимает заполнение вектора и 3.8 секунды собственно вычисление интеграла, отсюда и берется «худший результат из сравниваемых». Да и вообще сравнение получается немного странным:
1) Постоянный шаг, вычисление аргумента «по месту».
2) Постоянный шаг, вычисление аргумента где-то снаружи, но с учетом времени этого вычисления (!?).
3) 4) Постоянный шаг, вычисление аргумента «по месту» (если я правильно понимаю этот код, простите мое плохое знание С++ и ленивых вычислений).
5) Переменный шаг, вычисление аргумента «по месту».
То есть везде какие-то отличия по сути, что же мы сравниваем?
Но вообще спасибо за статью, узнал новое для себя!
P.S.: Я же верно понимаю, что для варианта 5 теряется совместимость с вектором?
А почему во всех вариантах вычисление сетки должно учитываться (по времени), а во втором — нет? Так нечестно.
std::vector
плох не только скоростью. Если интегрируемая функция достаточно тяжело вычисляется на каждом шаге, то общение с памятью не будет "бутылочным горлышком, конечно. Но может банально памяти не хватить под этот вектор, например.
5 вариант не потерял совместимость с вектором, просто t_i в вектор нужно положить другие (в смысле другие числа, а тип тот же; vector<double>
). Но работает это не быстро (14 секунд в моих условиях, из них 9 на заполнение вектора, из которых можно 1.5 секунды сэкономить, если сделать заранее reserve).
А сравниваем мы и скорость, и удобство, ну и потребление памяти, если хотите.
А почему во всех вариантах вычисление сетки должно учитываться (по времени), а во втором — нет? Так нечестно
Потому что во всех вариантах это именно вычисление, а во втором — работа с памятью. И в зависимости от свойств системы результат получается разным. Я бы сказал так, что либо второй вариант в вашей статье лишний, либо наоборот — все остальные лишние. И, разумеется, самой идеи рассказать таким образом о ranges это никак не умаляет.
std::vector плох не только скоростью
Конкретно вектор ничем вообще не плох. Плох (или «плох») любой способ формирования набора узлов, который требует промежуточной памяти. А тут уже нет никакого выбора — либо вам без памяти не обойтись (набор получен по сети), либо нет никакого смысла хранить его в памяти (и вычисляем на каждом шаге, любым из предложенных вами способов).
5 вариант не потерял совместимость с вектором
Пардон, но если посмотреть на код в for()?
Пардон, но если посмотреть на код в for()?
for (auto t: t_nodes | ranges::v3::views::drop(1))
работает и в том случае, если t_nodes
есть vector<double>
. О времени для такого варианта я выше и говорил.
Отбрасывает первый элемент и оставляет всё остальное.
Что вот мне совсем не нравится в ranges — очень скудная документация. В примерах у Ниблера drop даже не встречается. Но есть take — это, наоборот, взять n первых элементов, отбросив всё остальное. И оба они ленивые, то есть можно работать с бесконечной последовательностью, а уж потом сделать take, например.
Там основное время — заполнение вектора, а поскольку шаг в этом примере переменный, то функция step_f, вычисляющая t_i, уже не такая простая. Я подозреваю, что всё дело в ней. Сами же integrate в v2 и v5 тоже разные, честная версия из v5 заметно медленнее версии из v2, в которой используется тот факт, что шаги одинаковые. Но шагов в v5 меньше, чем в v2. В общем, довольно проблематично их сравнивать.
Ну и в очередной раз мы говорим о проводимых сравнениях. :)
Насчет модификаторов интервала, я не совсем понял (и сходу не нагуглил, точнее нагуглил нечто обратно противоположное) — это что-то общее для всех контейнеров С++, или только для интервалов?
Хм, ну подождите, если ставится задача «делаем универсальную функцию интегрирования трапециями, в которую снаружи передается набор узлов», то без трат памяти не обойтись.Не обязательно. Если снаружи передаётся короутина, то она может порождать набор «лениво».
А если стоит задача генерировать узлы «на месте», то можно генерировать их прямо в теле функции, а можно передать указатель на функцию-генератор (привет Си), а можно корутину, или вот ranges, или вообще что угодно еще, с примерно одинаковым результатом.
вообще что угодно еще, с примерно одинаковым результатом.Не совсем. Ranges — это некоторая абстракция, которая легко получается из корутин или генераторов.
Вообще мне кажется на практике ragnges, реализованные через корутины могут оказаться самым распространённым вариантом…
Зачем войд-звездочка и пара функций? Вот вам без звездочек и пары функций (сказал он с хитрой улыбкой):
#include <iostream>
double integrate(int generator()) {
for(int i {}; i < 42; i++) {
std::cout << 1.1 * static_cast<double>(generator()) << std::endl;
}
return 0.0;
}
int Generator()
{
static int count {};
return count++;
}
int main() {
integrate(Generator);
return 0;
}
А вы на каждый набор параметров генератора будете писать свою функцию?
Все зависит от обстоятельств. Если генератор в принципе должен быть параметризован (что не факт, и, кстати, выше это требование не предъявлялось), то это можно сделать разными способами, а можно не суметь сделать вообще.
А запускать вычисления в том же потоке ещё раз с нуля как будете?
Тут конечно не поспоришь, в приведенном выше примере придется мутить несколько более сложный генератор (и конечно в стиле "о Боже, что это").
range что с сеткой, что без неё всё равно остаётся последовательным.
А хотелось бы магии, чтобы для задач вроде подобной сгенерировался такой код, который разбил бы range на subranges, и запустил их в параллель на нескольких ядрах.
Но для этого сами исходники (итератор, который знает свои границы и умеет шагать вперёд) не очень хороши. Ну разве что сделать тестовый прогон без финальных вычислений, и там эти самые границы разметить (собрать таблицу значений итератора), а потом уже выдать каждому потоку/задаче по собственному диапазону/подтаблице. Но это уже не так прямолинейно.
P.S.
… а v4 в три раза быстрее, чем v1а вот это совсем странно.
Зачем нужны ranges из C++20 в простой числодробилке?