Pull to refresh

Comments 25

UFO just landed and posted this here
Да, скорее всего, построить SQL запрос на основании цепочки преобразований не получится, т.к. для этого нужно заглянуть в лямбду и посмотреть, что же она делает. В .net для этого компилятор строит дерево выражения (Expression Trees), в С++ такого нет.
Вот именно поэтому у вас и получился не LINQ, основная прелесть которого именно в AST.
Получилось некое подобие на Linq to object, с откровенно говоря, неудобным синтаксисом, и в большинстве своем не работающем IntelliSense. Но это лучше чем ничего, к тому же у меня интерес был скорее образовательный – погружение в новый с++11 и разминка мозгов. Результат оказался вполне работоспособный и сэкономил мне сотни строк явных циклов.
При достаточно глубоких decltype в какой-то момент IntelliSense отключался. В Visual Studio 2012 работало по-лучше, но все равно постоянно сталкивался с отсутствиями подсказок.
Получается вариация на тему парадокса всемогущества — можно ли написать такой сложный стандарт C++, что для него даже создатели Visual Studio не смогут сделать стабильный Intellisense? Очевидно, можно.
Создатели Visual Studio никогда не отличались большим мастерством в плане разработки IntelliSence для C++. Не знаю, как в 2012, но вплоть до 2010 нормально можно было пользоваться только Visual Assist'ом.
В 2012 не сильно лучше, к сожалению. Но падать оно стало меньше, хотя тормозит, и подвисает точно так же :)
p.s: тоже использую Visual Assist ка основной инструмент.
Скажем так. Это не получится в рамках действующего стандарта (C++11). В рамках C++14 — по идее, может получиться. Если объединить полиморфные лямбды и что-то в стиле expression templates (только в рантайме), то на выходе вполне можно будет иметь и сконструированный sql-запрос, и анализатор полученного рекордсета.
Осталось дождаться, когда полиморфные лямбды из C++14 где-нибудь реализуют, и фильтры совсем просто писать станет.
в полку не читаемых конвенций и заимствованных стилей прибыло.
А с Boost.Lambda получается ещё симпатичней…

#include «boost/lambda/lambda.hpp»
using namespace boost::lambda;

int data[3] = { 3, 1, 2 };
auto t1 = Linq::from(data).where(_1 > 1).orderBy(_1).toVector();
А чем Вас не устроил такой код, кроме того, что не похож на Linq?

boost::copy( data | filtered( [](const TestClass &a) { return a.iVal >= 5; } ), std::back_inserter(vec) );
boost::sort( vec, [](const TestClass &a1, const TestClass &a2) { return a1.sVal < a2.sVal; } ); 


Зачем тащить в язык куски C#? В C++ есть свои средства и, как минимум, не хуже.

Полный пример
#include <boost/range/adaptors.hpp>
#include <boost/range/algorithm.hpp>
#include <algorithm>
#include <iostream>
#include <vector>

struct TestClass
{
  int iVal;
  int sVal;
  
  TestClass( int val ): iVal(val), sVal(val) {}
};

int main()
{
  using namespace boost::adaptors;

  TestClass data[] = {7, 6, 4, 3, 8};
  std::vector<TestClass> vec;
  
  boost::copy( data | filtered( [](const TestClass &a) { return a.iVal >= 5; } ), std::back_inserter(vec) );
  boost::sort( vec, [](const TestClass &a1, const TestClass &a2) { return a1.sVal < a2.sVal; } ); 
  
  boost::transform( vec, std::ostream_iterator<int>(std::cout, " "), [](const TestClass &a) { return a.iVal; } );
  
  return 0;
}
Прорывная идея linq в том, что ступени конвеера знают друг о друге.
Если ты говоришь list.Where(...).First(), фильтр отрабатывает ровно до первого встреченного элемента.
Как с SQL — ты говоришь, что хочешь получить. Фреймворк вычисляет, как это получить наиболее оптимально.
А что там можно особо наоптимизировать? O(n) он не сможет уменьшить. Настоящая оптимизация начнётся тогда, когда можно алгоритм или контейнер заменить или подсчитать что-то заранее. На это только человек способен. Да и тот же select копирует элементы, хотя далеко не всегда это нужно.

Мне кажется то, что привычка использовать Linq приводит к значительному падению производительности кода. Посудите сами, когда так легко дважды скопировать Linq выражение, поменяв в нём пару байтов, будет ли программист заморачиваться? А получит в итоге два прохода по коллекции.
Оптимизировать например, OrderBy+First. Хотя, можно сразу написать Max с кастомным предикатом сравнения.
Место Linq там, где много рутины, вылизывать которую по скорости не имеет смысла. В любом серьёзном приложении это 80%

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

Так можно много чего запретить. Например, макросы. Ведь сходу не видно, какое выражение стоит за макросом и какая у него сложность вычисления. Есть опасность накопипастить.
Что за оптимизации такие, расскажите, желательно со ссылками на какие-либо исследования или источники от разработчиков.
С такими заявлениями желательно уточнять, о какой linq реализации идет речь, в том же linq to objects вы не получите практически никаких оптимизаций, насколько мне известно, ну, разве что лишний Select(x => x) будет убран или будет вызван Count, вместо обхода по коллекции и подсчета элементов.

Непонятно также о каком конвеере идет речь и я сильно сомневаюсь что «ступени» там что-то знаю друг о друге, разве что конкретный провайдер, зная запрос целиком, может как-то оптимизировать и перестраивать дальнейшие вычисления.
Ну, например, описанное выше "[в] list.Where(...).First(), фильтр отрабатывает ровно до первого встреченного элемента" — действительно работает. Только для того, чтобы это работало, ступеням ничего друг о друге знать не надо, это банальная работа с правильно построенными итераторами.

А вот как только начинается IQueryable — там в момент выполнения известно все дерево целиком, и можно его перестраивать любым образом (что, собственно, и происходит внутри любого ORM с LINQ-провайдером).
Производительность все-же будет отличаться, так как в моем примере сортироваться будут указатели, а не сами объекты. Если копирование объектов — это тяжелая операция, то разница будет.
В linq легко можно добавлять в конвеер обработки новые операции, и не заморачиваться с ручным управлением, того, куда направить промежуточный результат. Если можно обработать на лету — будет сдалано без дополнительных буферов, а если нужен, то будет промежуточный буфер из указателей. Да, вручную, возможно, получится написать эффективнее, но это потребует дополнительного времени разработки. В случае же linq производительность наверняка будет выше наивного подхода с выделением на каждом этапе буфера и копирования в него. Представленная реализация оптимизирует именно копирования, для int и double это, конечно ничего не дает, но вот для «тяжелых» объектов прирост скорости будет!
Далеко не факт то, что буфер указателей будет сортироваться быстрее:
1. Выделение памяти под буфер и заполнение его
2. Обращение к элементом через разыменование
3. Буфер указателей менее дружественен для кеша
4. Для тяжёлых объектов должна использоваться move-семантика

В случае же linq производительность наверняка будет выше наивного подхода с выделением на каждом этапе буфера и копирования в него.

В C++ это считается очень плохим стилем. Всегда стараются работать с теми же данными.
Всё-таки есть разница между тем, чтобы обработать один набор данных несколькими алгоритмами полностью, и обработать тот же набор частично до получения нужного результата? Ну, например, вместо алгоритма sort применять partial_sort или nth_element. Так же описанный в статье подход позволит реализовать концепцию ленивых вычислений (не обрабатывать весь набор данных сразу, а элемент за элементом по мере необходимости). Также он может упросить восприятие сложных конструкций, если над элементом данных надо последовательно выполнить несколько преобразований. В общем, хотя на первый взгляд ваш пример и пример из статьи эквивалентны, описанный в статье вариант обладает большем потенциалом.
Sign up to leave a comment.

Articles