Comments 23
Да и дженерики в отличие от шаблонов код не генерируют и не могут производить оптимизации частных случаев.
Для каждого value-типа JIT генерирует отдельную реализацию. Реализации переиспользуются только для reference-типов (то есть там, где всё равно будет указатель).
А вот миллионы лишних переключений и вызовов функций выкинуть некому.
Вообще-то JIT инлайнить умеет.
Так же не ясно, что именно имелось ввиду под "переключениями между контекстами", поскольку в классическом понимании переключение контекста происходит при переключениях в ядро ОС и обратно, прерываниях и смене текущего потока при использовании кооперативной многозадачности (вызов longjmp, например). На производительность итераторов в языке вышеописанное влиять не может.
Главная же проблема с производительностью в том, что JIT не может тратить минуты на компиляцию MSIL в нативный код, а потому применяемый набор оптимизаций ограничен. Некоторые вещи оптимизируются самим компилятором, например замена foreach на for, если тот был использован на массиве.
Так же следует понимать, что yield return генерирует не самую оптимальную реализацию итератора, в частности, из-за этого в методах из состава System.Linq.Enumerable используются итераторы, написанные вручную.
И если ваш код делает некоторые быстрые вычисления очень много раз, то накладные расходы будут заметны. В то время как при итерировании объектов БД у вас скорее всего такой проблемы не возникнет.
В C# итераторы более развитые и у них нет проблемы целостности.
С этого места поподробнее, пожалуйста.
Это не проблема, а преимущество. Это значит, что в плюсах итераторы правильно декомпозированы, и не хранят ничего лишнего. Например, алгоритму std::copy_n
не требуется информация о конце итераторов.
А если нужно обязательно знать конец — make_iterator_range
, и в путь.
Если в std::copy_n передать число больше, чем количество оставшихся элементов, то как он себя поведет?
Огромное преимущество: следить за валидностью итераторов вручную
Что значит "вручную"? Сформулируйте развёрнуто.
Если в std::copy_n передать число больше, чем количество оставшихся элементов, то как он себя поведет?
А зачем так делать?
- Вы должны где-то хранить конечный итератор и сравнивать с ним для проверки валидности. В шарпе если не удалось передвинуть енумератор дальше, то все.
- А как узнать сколько осталось до конца? мы же эту информацию не храним. Бежали с помощью итератора, получили SIGTERM, надо остаток сохранить, а как узнать сколько? В шарпе вообще нет гарантированного способа получить размер остатка, если применили методы, влияющие на длину последовательности.
copy(source | take(n), destination)
Итератор — он должен итерировать. А итерировать без знания когда нужно закончить — практически нецелесообразно. Я не согласен что в С++ мы имеем пример верной декомпозиции.
copy(source | take(n), destination)
Прекрасно, я только за.
Кстати, а переменная destination
здесь — это что?
Это так называемый OutputRange — объект реализующий метод put, принимающий значения соответствующего типа.
Очень хорошо. Тогда следующий вопрос: зачем ему быть диапазоном, если достаточно одного итератора?
Зачем ему быть итератором, если ему достаточно реализовать лишь один единственный метод? :-)
import std.stdio;
import std.outbuffer;
import std.conv;
import std.range;
import std.algorithm;
struct Producer
{
int i;
int count;
this( int count )
{
this.count = count;
}
auto empty()
{
return i >= count;
}
auto front()
{
return i;
}
auto popFront()
{
i++;
}
}
struct Consumer
{
void put( int i )
{
write( i );
}
}
void main()
{
Producer( 10 ).take( 5 ).copy( Consumer() ); // 01234
Producer( 5 ).take( 10 ).copy( Consumer() ); // 01234
Producer( 5 ).map!( to!string ).copy( stdout.lockingTextWriter ); // 01234
Producer( 5 ).map!( to!string ).joiner.write; // 01234
5.iota.map!( to!string ).joiner.write; // 01234
5.iota.each!write; // 01234
}
Идиома Ranges