Generics & Sort
От: romkol  
Дата: 31.10.05 16:25
Оценка:
Например есть класс:

class myGen
{
public int flt_no;
public string reis_no;

public myGen(int pFlt_no, string pReis_no)
{
this.flt_no = pFlt_no;
this.reis_no = pReis_no;
}
}

и есть лист:
List<myGen> list = new List<myGen>();

Как реализовать две сортировки по int полю и по string?
Re: Generics & Sort
От: krasin Россия  
Дата: 31.10.05 16:27
Оценка:
Здравствуйте, romkol, Вы писали:

R>Например есть класс:


R>class myGen

R>{
R> public int flt_no;
R> public string reis_no;

R> public myGen(int pFlt_no, string pReis_no)

R> {
R> this.flt_no = pFlt_no;
R> this.reis_no = pReis_no;
R> }
R>}

R>и есть лист:

R>List<myGen> list = new List<myGen>();

R>Как реализовать две сортировки по int полю и по string?


1. Можно реализовать интерфейс IComparable классом myGen
2. Можно написать свой Comparer для myGen
Re: Generics & Sort
От: Lloyd Россия  
Дата: 31.10.05 18:17
Оценка: 3 (1) +1
Здравствуйте, romkol, Вы писали:

R>и есть лист:

R>List<myGen> list = new List<myGen>();

R>Как реализовать две сортировки по int полю и по string?


lst.Sort(delegate(MyGen x, MyGen y) {return (x.FltNo != y.FltNo) ? x.FltNo - y.FltNo : string.Compare(x.ReisNo, y.ReisNo);});
... << RSDN@Home 1.1.4 stable rev. 510>>
Re[2]: Generics & Sort
От: _FRED_ Черногория
Дата: 01.11.05 10:15
Оценка:
Здравствуйте, Lloyd, Вы писали:

R>>и есть лист:

R>>List<myGen> list = new List<myGen>();

R>>Как реализовать две сортировки по int полю и по string?


L>lst.Sort(delegate(MyGen x, MyGen y) {return (x.FltNo != y.FltNo) ? x.FltNo - y.FltNo : string.Compare(x.ReisNo, y.ReisNo);});


Только не забыть, что MyGen — класс и в списке может быть null.
<< RSDN@Home 1.2.0 alpha rev. 616 >> =01:15= [Windows 2003 — 5.2.3790.65536]
under «*none*»
Help will always be given at Hogwarts to those who ask for it.
Re[2]: Generics & Sort
От: Mab Россия http://shade.msu.ru/~mab
Дата: 01.11.05 10:52
Оценка: +1
Здравствуйте, Lloyd, Вы писали:

Хороший способ, только сделует иметь в виду, что он медленнее, чем передача компаратора.
Re[3]: Generics & Sort
От: Lloyd Россия  
Дата: 01.11.05 11:20
Оценка:
Здравствуйте, Mab, Вы писали:

Mab>Хороший способ, только сделует иметь в виду, что он медленнее, чем передача компаратора.


Насколько я помню, вызов делегата не существенно дороже вызова виртуального метода.
... << RSDN@Home 1.1.4 stable rev. 510>>
Re[4]: Generics & Sort
От: Mab Россия http://shade.msu.ru/~mab
Дата: 01.11.05 12:37
Оценка: 146 (13) +1
Здравствуйте, Lloyd, Вы писали:
L>Насколько я помню, вызов делегата не существенно дороже вызова виртуального метода.
Все познается в сравнении Поскольку данная тема (оптимизация алгоритмов при использовании generics) мне близка, то дам более развернутый ответ.

Я провел некоторые тесты для сравнения скорости работы Array.Sort (реализующего qsort) при разных способах вызова, а также ряда самописных реализаций.

Вот результаты:

Array.Sort with ComparerClass: 4640
Array.Sort with ComparerStruct: 4984
Array.Sort with delegate: 6687
QSort with ComparerClass: 3359
QSort with ComparerStruct: 3125
SmartQSort with LessComparerClass: 3015
SmartQSort with LessComparerStruct: 1719


Поясню о чем речь. Производится сортировка массива из 10 миллионов случайных чисел (массив всегда один и тот же для всех участников испытания). Алгоритм сортировки -- классический qsort с детерминированным выбором разделителя (выбирается всегда средний элемент).

Первый две строки соответствуют реализации, когда компаратор -- это класс или структура соответственно. Третья строка -- использование делегата. Виден проигрыш в производительности (он действительно не такой большой).

Но небольшой он лишь при использовании тупой реализации Array.Sort. Существует известный трюк, позволяющий избавиться от виртуальности вызова при использовании интерфейсов. А именно, пишем:
    static void QSort<T, Comparer>(T[] arr, Comparer cmp)
    where Comparer : IComparer<T>

Как видно, теперь у нас два параметра-типа -- тип элемента массива и тип компаратора. Дело в том, что если в качестве компаратора передавать структуру, реализующую интерфейс, то начиная кажется со второй беты боксинга происходить не будет. Соответственно, мы получим обычный, неполиморфный вызов. Это и видно на представленной таблице -- ускорение 3125 против 4640.

Но и это еще не все. IComparer удобен конечно, но совершенно отвратителен с точки зрения производительности. Лучших результатов можно добиться с использованием двузначного предиката IsLess. Он проще вычисляется и открывает возможности JIT-у заинлайнить код компаратора (это лишь мои подозрения, впрочем).

Итак, смотрим последнюю строку -- 1719. Это почти в 2.7 раз быстрее, чем было изначально при использовании компаратора-класса с полиморфным вызовом!

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

Исходник теста:
using System;
using System.Collections.Generic;


class Test
{
    interface ILessComparer<T>
    {
        bool IsLess(T lhs, T rhs);
    }
    
    static void QSort<T, Comparer>(T[] arr, int lo, int hi, Comparer cmp)
    where Comparer : IComparer<T>
    {
        int i = lo;
        int j = hi;
        T sep = arr[(lo + hi) >> 1];
        while (i <= j)
        {
            while (cmp.Compare(arr[i], sep) < 0)
                i++;
            while (cmp.Compare(sep, arr[j]) < 0)
                j--;
            if (i <= j)
            {
                T tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
                i++;
                j--;
            }        
        }
        
        if (lo < j)
            QSort(arr, lo, j, cmp);
        if (i < hi)
            QSort(arr, i, hi, cmp);    
    }
    
    static void QSort<T, Comparer>(T[] arr, Comparer cmp)
    where Comparer : IComparer<T>
    {
        QSort(arr, 0, arr.Length - 1, cmp);
    }    
    
    static void SmartQSort<T, Comparer>(T[] arr, int lo, int hi, Comparer cmp)
    where Comparer : ILessComparer<T>
    {
        int i = lo;
        int j = hi;
        T sep = arr[(lo + hi) >> 1];
        while (i <= j)
        {
            while (cmp.IsLess(arr[i], sep))
                i++;
            while (cmp.IsLess(sep, arr[j]))
                j--;
            if (i <= j)
            {
                T tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
                i++;
                j--;
            }        
        }
        
        if (lo < j)
            SmartQSort(arr, lo, j, cmp);
        if (i < hi)
            SmartQSort(arr, i, hi, cmp);    
    }
    
    static void SmartQSort<T, Comparer>(T[] arr, Comparer cmp)
    where Comparer : ILessComparer<T>
    {
        SmartQSort(arr, 0, arr.Length - 1, cmp);
    }    
    
    class ComparerClass : IComparer<int>
    {
        public int Compare(int a, int b)
        {
            return a < b ? -1 : (a > b ? +1 : 0);
        }
    }
    
    struct ComparerStruct : IComparer<int>
    {
        public int Compare(int a, int b)
        {
            return a < b ? -1 : (a > b ? +1 : 0);
        }
    }
    
    class LessComparerClass : ILessComparer<int>
    {
        public bool IsLess(int a, int b)
        {
            return a < b;
        }
    }
    
    struct LessComparerStruct : ILessComparer<int>
    {
        public bool IsLess(int a, int b)
        {
            return a < b;
        }
    }
    
    const int N = 10000000;
    static int[] arr = new int[N];
    
    
    delegate void Tester(int[] arr);

    static void InvokeTest(Tester tester, string comment)
    {
        Random random = new Random();
        for (int i = 0; i < arr.Length; i++)
            arr[i] = random.Next();
        int timer0 = Environment.TickCount;
        tester(arr);
        int timer1 = Environment.TickCount;
        Console.WriteLine("{0}: {1}", comment, timer1 - timer0);
        for (int i = 0; i < arr.Length - 1; i++)
            if (arr[i] > arr[i+1])
                throw new Exception("Oops..");
    }

    static void Main()
    {
        InvokeTest(
            delegate (int[] arr)
            {
                Array.Sort(arr, new ComparerClass());
            },
            "Array.Sort with ComparerClass");
            
        InvokeTest(
            delegate (int[] arr)
            {
                Array.Sort(arr, new ComparerStruct());
            },
            "Array.Sort with ComparerStruct");
            
        InvokeTest(
            delegate (int[] arr)
            {
                Array.Sort(
                    arr,
                    delegate (int lhs, int rhs)
                    {
                        return lhs < rhs ? -1 : (lhs > rhs ? +1 : 0);
                    });    
            },
            "Array.Sort with delegate");
            
        InvokeTest(
            delegate (int[] arr)
            {
                QSort(arr, new ComparerClass());
            },
            "QSort with ComparerClass");
            
        InvokeTest(
            delegate (int[] arr)
            {
                QSort(arr, new ComparerStruct());
            },
            "QSort with ComparerStruct");
            
        InvokeTest(
            delegate (int[] arr)
            {
                SmartQSort(arr, new LessComparerClass());
            },
            "SmartQSort with LessComparerClass");
            
        InvokeTest(
            delegate (int[] arr)
            {
                SmartQSort(arr, new LessComparerStruct());
            },
            "SmartQSort with LessComparerStruct");
    }
}
Re[5]: Generics & Sort
От: Mab Россия http://shade.msu.ru/~mab
Дата: 01.11.05 19:21
Оценка: 25 (1)
Здравствуйте, Mab, Вы писали:

Mab>Но и это еще не все. IComparer удобен конечно, но совершенно отвратителен с точки зрения производительности. Лучших результатов можно добиться с использованием двузначного предиката IsLess. Он проще вычисляется и открывает возможности JIT-у заинлайнить код компаратора (это лишь мои подозрения, впрочем).


Добавлю еще про инлайнинг. Наличие или отсутствие его можно понять, добавив для метода атрибут MethodImpl(MethodImplOptions.NoInlining). Если это сделать, то становится ясно, что единственный из методов в приведенном примере, который инлайнится -- это лишь последний вариант теста. Что помешало заинлайнить вариант с трехзначным компаратором -- остается только догадываться. Возможно слишком большое ветвление.
Re[4]: Generics & Sort
От: TK Лес кывт.рф
Дата: 01.11.05 19:59
Оценка: 1 (1) -1
Здравствуйте, Lloyd, Вы писали:

Mab>>Хороший способ, только сделует иметь в виду, что он медленнее, чем передача компаратора.

L>Насколько я помню, вызов делегата не существенно дороже вызова виртуального метода.

В текущей реализации будет делегат будет вызываться через виртуальный метод. т.е. если есть желание избавиться от лишней косвенности, то лучше использовать компаратор
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[5]: Generics & Sort
От: VladD2 Российская Империя www.nemerle.org
Дата: 04.11.05 00:01
Оценка: 8 (1) +1 :)
Здравствуйте, Mab, Вы писали:

Скажу пару слов в защиту делегатов... Не так уж они сильно медленее при условии использования нормального алгоритма сортировки. Просто то что использовано во фрэймворке не очень оптимальный вариант.

Добавил в твой тест еще один варинт SmartQSort использующий делегат. Результат получился следующим:
Array.Sort with ComparerClass: 4422
Array.Sort with ComparerStruct: 4891
Array.Sort with delegate: 6344
QSort with ComparerClass: 2922
QSort with ComparerStruct: 2578
SmartQSort with LessComparerClass: 2672
SmartQSort with LessComparerStruct: 1500
SmartQSort with IsLess<T> delegate: 3000


Код:
    delegate bool IsLess<T>(T x, T y);

    static void SmartQSort<T>(T[] arr, IsLess<T> isLess)
    {
        SmartQSort(arr, 0, arr.Length - 1, isLess);
    }

    static void SmartQSort<T>(T[] arr, int lo, int hi, IsLess<T> isLess)
    {
        int i = lo;
        int j = hi;
        T sep = arr[(lo + hi) >> 1];
        while (i <= j)
        {
            while (isLess(arr[i], sep))
                i++;
            while (isLess(sep, arr[j]))
                j--;

            if (i <= j)
            {
                T tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
                i++;
                j--;
            }
        }

        if (lo < j)
            SmartQSort(arr, lo, j, isLess);
        if (i < hi)
            SmartQSort(arr, i, hi, isLess);
    }


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

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

Плюс нужно понимать, что столь большие отрывы достигаются в следствии того, что сортируются целые числа. Они и сравниваются и перемещаются куда быстрее. На массиве объектов по идее разрыв должен стать меньше. Так что предостеригаю всех от поспешного переписывания кода на рукопашный. Делайте это только если сортировка действительно узкое место в программе.
... << RSDN@Home 1.2.0 alpha rev. 618>>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[6]: Generics & Sort
От: Mab Россия http://shade.msu.ru/~mab
Дата: 05.11.05 15:31
Оценка: 22 (2)
Здравствуйте, VladD2, Вы писали:

Чтобы подвести данный тест к логическому завершению, приведу еще результаты сравнения с C++ (managed и native, версии 7.1 и 8.0 RC). Так можно понять, заодно, насколько вообще осмысленно говорить о том, чтобы использовать C# 2.0 для написаня критического по скорости кода, который до сих пор писался на C++.

Участники испытания:
Все те же, что и в пролшый раз, но со следующими добавлениями:
1) предложенный Владом вариант с делегатом и двузначным сравнением
2) почти дословно переведенный на C++ вариант SmartQSort
3) std::sort из STL (вне конкурса, т.к. реализует иной, более быстрый алгоритм)
4) SpecializedQSort на C# -- получен ручной подстановкой типа int и заменой вызова компаратора на прямое сравнение

Решаемая задача:
Детерминированный QSort без попыток оптимизации (нет tail recursion elimination, нет перехода на квадратичную сортировку на малых отрезках). Тестируются два варианта: сортировка 10 000 000 целых и сортировка 100 000 000 целых. В первом случае тест повторяется 10 раз и берется среднее. Исходные данные для всех испытуемых одинаковы -- детерминированные псевдослучайные числа, полученные с помощью линейного конгруэнтного метода (взят кусок реализции из VC CRT).

Тестовая конфигурация:
AMD Athlon 64 3200+ 1 GB DDR RAM (PC3200)

Компиляторы: VS.NET 2003, VS.NET 8.0 RC.

Ключи компиляции:
C#              /optimize+
VC7.1 managed   /clr /Ox /EHsc
VC7.1 native    /Ox /EHsc
VC8.0 managed   /clr /Ox /EHsc
VC8.0 native    /Ox /EHsc


Результаты для C#:
Array.Sort with ComparerClass                  3921      42656
Array.Sort with ComparerStruct                 4289      47500
Array.Sort with delegate                       5773      64828
QSort with ComparerClass                       2756      30062
QSort with ComparerStruct                      2544      27843
SmartQSort with LessComparerClass              2503      27157
SmartQSort with LessComparerStruct             1312      14235
SmartQSort with delegate                       2848      31188
SpecializedQSort                               1251      13516

Результаты для C++:
std::sort managed vc71                         2156      22640
SmartQSort managed vc71                        1159      12657
std::sort native vc71                          1179      12125
SmartQSort native vc71                         1178      12906
std::sort managed vc80                         1318      13500
SmartQSort managed vc80                        1340      14484
std::sort native vc80                          1146      11891
SmartQSort native vc80                         1189      13047


Время указано в миллисекундах. Второй столбец -- размер массива 10 000 000. Третий -- 100 000 000.

Общая сводная таблица (сортировка по второму столбцу):
std::sort native vc80                          1146      11891
SmartQSort managed vc71                        1159      12657
SmartQSort native vc71                         1178      12906
std::sort native vc71                          1179      12125
SmartQSort native vc80                         1189      13047
SpecializedQSort                               1251      13516
SmartQSort with LessComparerStruct             1312      14235
std::sort managed vc80                         1318      13500
SmartQSort managed vc80                        1340      14484
std::sort managed vc71                         2156      22640
SmartQSort with LessComparerClass              2503      27157
QSort with ComparerStruct                      2544      27843
QSort with ComparerClass                       2756      30062
SmartQSort with delegate                       2848      31188
Array.Sort with ComparerClass                  3921      42656
Array.Sort with ComparerStruct                 4289      47500
Array.Sort with delegate                       5773      64828


Выделены некоторые интересные, на мой ввзгляд, строки.

Кое-какие выводы:
1) Лучшая generic-реализация алгоритма на C# уступает лучшей native-реализации на C++ по скорости 15%.
2) При переходе от VC 7.1 к VC 8.0 RC наблюдается некоторый регресс в производительности (интересно было бы узнать, как дела в VC 8.0 Release).
3) Специализированная реализация алгоритма на C# уступает native-реализации на C++ по скорости 10%. Заметим, что при этом первая явлется полностью safe (например, включены array bounds checks).
4) Managed-реализация std::sort устуает native-релизации std::sort под VC7.1 примерно два раза. Странно, но факт. Возможно, порождается слишком лохматый IL-код, а возможно и недоработка JIT. Так или иначе, в FW 2.0 эти проблемы решены (хотя, см. п.2).
5) Использование в таком алгоритме делегата замедляет код примерно в 1.5-2 раза (чем оптимальнее реализация без делегата, тем выше коэффициент).
6) std::sort вместо SmartQSort выигрывает незначительно и лишь при очень больших размерах массива.

Исходники тестов:
C#
C++

Буду рад любым комментариям.
Re[7]: Generics & Sort
От: Аноним  
Дата: 06.11.05 14:28
Оценка:
Здравствуйте, Mab, Вы писали:

Mab>Исходники тестов:

Mab>C#
Mab>C++

Mab>Буду рад любым комментариям.


Не занимайтесь ерундой. Это не будет акрутально ни в этой версии .NET FW, но в следующей. Такие оптимизации в реальных проектах просто не нужны.
Re[8]: Generics & Sort
От: Mab Россия http://shade.msu.ru/~mab
Дата: 06.11.05 14:35
Оценка:
Здравствуйте, Аноним, Вы писали:
А>Такие оптимизации в реальных проектах просто не нужны.
Думаю, стоило бы добавить ИМХО.
В тех проектах, где я участвую, скорость имеет значение. И заказчик готов платить, если вместо часа обсчет графа будет произведен за полчаса. А значит такой проект вполне реальный
Re[8]: Generics & Sort
От: VladD2 Российская Империя www.nemerle.org
Дата: 07.11.05 23:54
Оценка:
Здравствуйте, <Аноним>, Вы писали:

А>Не занимайтесь ерундой. Это не будет акрутально ни в этой версии .NET FW, но в следующей. Такие оптимизации в реальных проектах просто не нужны.


Да? А я вот в редакторе кода для сортировки стилей вообще КаунтингСорт использовал, да еще специализированную версию (чтобы уж наверняка небыло никаких левых рассчетов).

И это не было причудой. Просто сортировка применяется в месте которое может быть вызвано очень много раз.
... << RSDN@Home 1.2.0 alpha rev. 618>>
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.