пятница, 29 февраля 2008 г.

Сравнение производительность С#,C,C++ часть 3, результаты

В предыдущих сериях:
Разединяем строки...обьединяем строки.
Сравнение производительность С#,C,C++ часть 1, код на C#
Сравнение производительность С#,C,C++ часть 2, код на C++ и на С

Изначально идея была навеяна STL vector performance

Цели которые я перед собой ставил - сравнить производительность кода на чистом С с кодом на С++ с использованием STL/boost при работе со строками на более-менее похожей на реальную задаче. C# я добавил в тесты когда увидел огромную разницу в производительности.

Напомню задачу:
нужно из таких строк: "word1-word2", "word3-word4", "word5-word6"
получить вот такую строку:
word1:word2:word3:word4:word5:word6

Код на С.С++ я компилил в VS 2005, с оптимизацием по скорости в Release версии. Код на С# в VS 2008 Express Edition.

Сводная табличка результатов, отсортированная по скорости.







boost::split, boost::join0.596665 sec
boost::split, boost::join, no std:copy0.492545 sec
simple select_many0.100862 sec
boost::tokenizer0.086117 sec
plain C++0.053814 sec
C#0,041314 sec
plain C0.003925 sec


Выводы - я повременю с уходом в монастырь только потому что код на С все-таки оказался в 10 раз быстрее кода на С#. И в принципе его еще можно немного разогнать.
Хотя код на C# я не оптимизировал, даже скорее наоборот, написал максимально "красиво" с использованием лямбд/генериков не заботясь о скорости. Цели написать быстрый код на C# не было. Когда я переписал с испльзованием StringBuilder разница по скорости с кодом на С сократилась до 4х раз. Уверен его можно еще разогнать.

Что же касается кода на С++ с испльзованием STL+boost - либо я не знаком с каким-то особым способом его использования/оптимизации...либо все очень и очень плохо.

Особенно удручающе выглядит использование Boost String Algorithms Library.

Послесловие - претензии к коду на С++ принимаются только если будет предложено что-то лучшее :). Я старался писать в "идиоматичном" С++, по крайнем мере как я его понимаю. Если кто-то может написать более эффективную реализацию на С++ с использованием std::string, std::vector и прочего std::, а также желательно boost и при этом не скатываясь к варианту на С - буду только рад.
Претензии к реализации тоже не очень принимаются...я и сам вижу что результат будет не правильным например на такой строке "word1--word2" или на такой "word1-word2-" и т.д. Но я решал конкретную задачу а не писал обобщенный способ разбиения строк.
Также я прекрасно вижу что алгоритмы на С++ и C# отличаются от алгоритма на С. Но - и тут я старался написать на "идиоматичном" С, и получилось именно вот так.

Сравнение производительность С#,C,C++ часть 2, код на C++ и на С

Напомню задачу:
нужно из таких строк: "word1-word2", "word3-word4", "word5-word6"
получить вот такую строку:
word1:word2:word3:word4:word5:word6


Использовал 6 разных вариантов решения этой задачи.
1. C использованием boost/algorithm/string
2. C использованием boost/algorithm/string, но обрабатывал промежуточный результат
3. Использовал select_many из Разединяем строки...обьединяем строки.
4. Использовал boost/tokenizer
5. Использовал реализацию "в лоб" на С++.
6. Реализовал все это дело практически на чистом С для частного случая.

Код:

#include "stdafx.h"

 

#include <string>

#include <algorithm>

 

#include <boost/tokenizer.hpp>

#include <boost/algorithm/string.hpp>

#include <boost/bind.hpp>

 

 

class HiPerfTimer

{

public:

    void start()

    {

        QueryPerformanceFrequency(&freq);

        QueryPerformanceCounter(&begin);

    }

    void stop(){QueryPerformanceCounter(&end);}

 

    double duration(){return (double)(end.QuadPart-begin.QuadPart)/freq.QuadPart;}

protected:

    LARGE_INTEGER begin,end,freq;

};

 

 

template<class _container_type,class _Fn1> inline

_container_type select_many(const _container_type& container, _Fn1& _Func)

{   

    _container_type result;

    for(_container_type::const_iterator v = container.begin();v!=container.end();++v)

    {

        _container_type tmp = _Func(*v);

        for(_container_type::const_iterator v1 = tmp.begin();v1!=tmp.end();++v1)

            result.push_back(*v1);

    }

    return result;

}

 

std::vector<std::string>    split_string    (const std::string& str, const std::string& delimiters)

{

    std::vector<std::string> v;

    size_t offset = 0;

    while(true)

    {

        size_t token_start = str.find_first_not_of(delimiters, offset);

        if (token_start == std::string::npos)

        {

            v.push_back( str.substr(token_start,str.length()  - token_start));

            break;

        }

        size_t token_end = str.find_first_of(delimiters, token_start);

        if (token_end == std::string::npos)

        {

            v.push_back(str.substr(token_start));

            break;

        }

        v.push_back(str.substr(token_start, token_end - token_start));

        offset = token_end;

    }

 

    return v;

}

 

std::string    join_string        (const std::vector<std::string>& v,const std::string& delimiters)

{

    std::string s;

    size_t reserve = 0;

    for(std::vector<std::string>::const_iterator the_str = v.begin();the_str!=v.end();++the_str)

        reserve+=the_str->length();

    reserve+=delimiters.length()*v.size();

    s.reserve(reserve);

    for(std::vector<std::string>::const_iterator the_str = v.begin();the_str!=v.end();++the_str)   

    {

        if(!s.empty())

            s+=delimiters;

        s+=*the_str;

    }

    return s;

}

 

 

char* do_the_job_in_plain_c(char** array, size_t size, char c1,char c2)

{

    size_t desc_size=size;

 

    for(size_t i=0;i<size;++i)

        desc_size+=strlen(array[i]);

    char* res_str = new char[desc_size];

    char* res_str_t = res_str;

    for(size_t i=0;i<size;++i,*res_str_t++ = c2)

    {

        char* p = array[i];

        do

        {

            *res_str_t++ = (*p == c1)?c2:*p;

        }while(*(++p));

    }

    res_str[desc_size-1] = 0;

    return res_str;

}

 

#define NUM_SAMPLES 10000

void test_select_many(std::vector<std::string>& w){

    LARGE_INTEGER begin,end,freq;

    double duration;

    QueryPerformanceFrequency(&freq);

 

    QueryPerformanceCounter(&begin);

    for (int i = 0; i < NUM_SAMPLES; ++i)

        std::string res = join_string(select_many(w,bind(split_string,_1,("-"))),":");

    QueryPerformanceCounter(&end);

 

    duration = (double)(end.QuadPart-begin.QuadPart)/freq.QuadPart;

    printf("select_many- %f sec\n",duration);

}

 

void test_boost_string_algorithm(std::vector<std::string>& w){

    HiPerfTimer pt;

    pt.start();

 

    for (int i = 0; i < NUM_SAMPLES; ++i)

    {

        std::string result;

        typedef std::vector< std::string > split_vector_type;

        split_vector_type split_vec_res;

        for(std::vector<std::string>::const_iterator v = w.begin();v!=w.end();++v){

            split_vector_type split_vec;

            boost::split( split_vec, *v, boost::is_any_of("-") );

            std::copy(split_vec.begin(),split_vec.end(),std::back_inserter(split_vec_res));

        }

        std::string res = boost::join(split_vec_res,":");

    }

    pt.stop();

    printf("boost::split, boost::join- %f sec\n",pt.duration());

}

 

void test_boost_string_algorithm_opt(std::vector<std::string>& w)

{

    HiPerfTimer pt;

    pt.start();

 

    for (int i = 0; i < NUM_SAMPLES; ++i) {

        std::string result;

        typedef std::vector< std::string > split_vector_type;

        split_vector_type split_vec_res;

        for(std::vector<std::string>::const_iterator v = w.begin();v!=w.end();++v){

            split_vector_type split_vec;

            boost::split( split_vec, *v, boost::is_any_of("-") );

            if(!result.empty())

                result+=":";

            result += boost::join(split_vec_res,":");

 

        }

    }

    pt.stop();

 

    printf("boost::split, boost::join, no std:copy- %f sec\n",pt.duration());

}

 

 

void test_boost_tokenizer(std::vector<std::string>& w)

{

    HiPerfTimer pt;

    pt.start();

 

    typedef boost::tokenizer<boost::char_separator<char> > tokenizer;

    for (int i = 0; i < NUM_SAMPLES; ++i) {

        std::string result;

        for(std::vector<std::string>::const_iterator v = w.begin();v!=w.end();++v){

            boost::char_separator<char> sep("-");

            tokenizer tok(*v,sep);   

            for (tokenizer::iterator tok_iter = tok.begin();tok_iter != tok.end(); ++tok_iter){

                if(!result.empty())

                    result+=":";

                result+=*tok_iter;

            }

        }

    }

    pt.stop();   

    printf("boost::tokenizer - %f sec\n",pt.duration());

}

 

void test_plain_cpp(std::vector<std::string>& w)

{

    HiPerfTimer pt;

    pt.start();

 

    for (int i = 0; i < NUM_SAMPLES; ++i) {

        std::string result;

 

        size_t length = w.size()+1;

        for(std::vector<std::string>::const_iterator v = w.begin();v!=w.end();++v)

            length+=v->length();

        result.reserve(length);           

 

        for(std::vector<std::string>::const_iterator v = w.begin();v!=w.end();++v){

            const std::vector<std::string>& w1 = split_string(*v,"-");

            for(std::vector<std::string>::const_iterator v1 = w1.begin();v1!=w1.end();++v1){

                if(!result.empty())

                    result+=":";

                result+=*v1;

            }

        }

    }

    pt.stop();   

    printf("plain C++ - %f sec\n",pt.duration());

}

 

void test_plain_c(char** array,size_t size)

{

    HiPerfTimer pt;

    pt.start();

    for (int i = 0; i < NUM_SAMPLES; ++i)

        std::auto_ptr<char> res_str ( do_the_job_in_plain_c(array,size,'-',':') );

    pt.stop();   

    printf("plain C - %f sec\n",pt.duration());

}

int _tmain(int argc, _TCHAR* argv[])

{

    std::vector<std::string> w;

    w.push_back("word1-word2");w.push_back("word3-word4");w.push_back("word5-word6");

    w.push_back("word7-word8");w.push_back("word9-word0");

 

    char* w1[]={"word1-word2","word3-word4","word5-word6","word7-word8","word9-word0"};

 

    test_boost_string_algorithm(w);

    test_boost_string_algorithm_opt(w);

    test_select_many(w);

    test_boost_tokenizer(w);

    test_plain_cpp(w);

    test_plain_c(w1,5);

    return 0;

}

Сравнение производительность С#,C,C++ часть 1, код на C#

Задумал я страшное - сравнить производительность чистого С, С++ (с использованием STL и boost) и С# на основании задачи из заметки Разединяем строки...обьединяем строки.

Напомню задачу:
нужно из таких строк: "word1-word2", "word3-word4", "word5-word6"
получить вот такую строку:
word1:word2:word3:word4:word5:word6

Для сравнения производительности время выполнения замерял с помощью QueryPerformanceCounter, просто потому что этот способ одинаков для С# и С++.

Основной код - код, делающий всю работу:

string r = ":".join(array1.SelectMany(x => x.Split('-')));


Весь исходный код на C#

using System;

using System.Linq;

using System.Collections.Generic;

using System.Runtime.InteropServices;

 

namespace MainProgramm

{

    static class string_extentions

    {

        public static string join(this string a, IEnumerable<string> elems)

        {

            string result = "";

            foreach (string elem in elems) {

                if (result.Length != 0)

                    result += a;

                result += elem;

            }

            return result;

        }

    }

    internal class HiPerfTimer

    {

        [DllImport("Kernel32.dll")]

        private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

 

        [DllImport("Kernel32.dll")]

        private static extern bool QueryPerformanceFrequency(out long lpFrequency);

 

        private long startTime, stopTime;

        private long freq;

        public HiPerfTimer(){QueryPerformanceFrequency(out freq);}

        public void    start   (){QueryPerformanceCounter(out startTime);}

        public void    stop    (){QueryPerformanceCounter(out stopTime);}

        public double   duration{

            get{return (double)(stopTime - startTime) / (double)freq;}

        }

    }

 

    class Program

    {

        static void Main(string[] args){

            string[] array1 = { "word1-word2", "word3-word4", "word5-word6", "word7-word8", "word9-word0" };

            HiPerfTimer pt = new HiPerfTimer();

            pt.start();

            for (int i = 0; i < 10000; ++i){

                string r = ":".join(array1.SelectMany(x => x.Split('-')));

            }

            pt.stop();

 

            Console.WriteLine("C# - {0} sec\n", pt.duration); // print the duration of the timed code

        }

    }

}

Сумбурные мысли о С++ , или С++ как функциональный язык

В сущности это продолжение записи "Разъединяем строки, объединяем строки..."

Функциональные возможности языка С++ очень ограничены и слабы. Но несмотря на это я раз за разом вижу как фанаты С++ отстаивают право С++ быть самым лучшем фукнциональным языком... Диалог обычно происходит следующим образом:
- Вот как это делается в Haskell
- Ха, в С++ можно сделать вот так, это практически то же самое.

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

Проблема С++ и его фанатов - славное прошлое языка С++. Было время когда С++ был безоговорочный "номер первый" в программировании. Причем не такой "новый первый" каким сейчас является..наверно java.Или VB, не в курсе. С++ применялся во всех областях и для всех задач. Были конечно и другие языки, которые где-то там как-то там применялось...но доминирование С++ было подавляющим. А остальные языки были как говорится никто и звали их никак.

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

Ну и что, все так плохо? Да нет вобщем-то. Для примера - никого ведь особо не беспокоит что Bentley не очень хорошо подходит для перевозки крупногабаритных грузов...

Кстати. Java - тоже вполне себе функциональный язык!!! На мем можно писать функционально!!!

Например вот так (источник вдохновения):

public int calcPriceInternalIterator(List items)

{

    Iterator i = items.iterator();

    UnaryFunction getItemPrice = new UnaryFunction()

    {

        public Object evaluate (Object obj)

        {

            return new Double(((SETLItem)obj).getPrice());

        }

    };

    Constant usd100 = new Constant(new Double(100));

    BinaryPredicateUnaryPredicate moreThanUSD100 = new BinaryPredicateUnaryPredicate

        (new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual(), getItemPrice, usd100));

    FilteredIterator fi = new FilteredIterator(i, moreThanUSD100);

    Summer addPrice = new Summer();

    Algorithms.foreach(fi, addPrice);

    return addPrice.getSum();

}


Обьясняю шутку...
Python "почти" эквиваленты:

a = [123,98,12,124,1007,2344]

 

sum1  = sum(filter(lambda x: x > 100,a))

sum2  = sum([x for x in a if x > 100])

sum3  = reduce( lambda x,y:x+y,[x for x in a if x > 100])

sum4  = reduce( lambda x,y:x+y,filter(lambda x: x > 100,a))


С# - фукнционально+LINQ

List<int> a = new List<int> { 123, 98, 12, 124, 1007, 2344 };

 

var sum1 = (from x in a where x > 100 select x).Sum();

var sum2 = a.filter(x => x > 100).reduce((x, y) => x + y);

четверг, 28 февраля 2008 г.

Разединяем строки...обьединяем строки.

Итак...задачка. Есть массив строк. Каждую строку в массиве нужно разделить по указанному разделителю, потом все полученные строки обьединить по еще одному указанному разделителю.

Если не очень понятно - нужно из таких строк: "word1-word2", "word3-word4", "word5-word6"
получить вот такую строку:
word1:word2:word3:word4:word5:word6

C#:

Увы, стандартная функция Join не работает с IEnumerable. Поэтому пришлось написать свою. Также нет стандартной функции reduce. Поэтому пришлось написать свою. Это не так уж и важно, поскольку к выводам не относится. Итак, код:

Способ1:

string[] a = { "word1-word2", "word3-word4", "word5-word6" };

Console.WriteLine(":".join(a.SelectMany(x=>x.Split('-'))));


Способ 2:

string[] a = { "word1-word2", "word3-word4", "word5-word6" };

Console.WriteLine(a.SelectMany(x => x.Split('-')).reduce((x, y) => x + ":" + y));



Python. Тут ничего дописывать не пришлось.

Способ 1:

a = ["word1-word2", "word3-word4", "word5-word6"]

print ":".join([x for word in a for x in word.split('-')])


Способ 2:

a = ["word1-word2", "word3-word4", "word5-word6"]

print reduce(lambda x,y:x+":"+y,[x for word in a for x in word.split('-')])



К чему это я...
С++, решение "в лоб"

    vector<string> w;

    w.push_back("word1-word2");

    w.push_back("word3-word4");

    w.push_back("word5-word6");

 

    string result;

 

    for(vector<string>::const_iterator v = w.begin();v!=w.end();++v)

    {

        const vector<string>& w1 = split_string(*v,"-");

        for(vector<string>::const_iterator v1 = w1.begin();v1!=w1.end();++v1)

        {

            if(!result.empty())

                result+=":";

            result+=*v1;

        }

    }

    cout << result << endl;


Попробуем функционально? "Без проблем":

    vector<string> w;

    w.push_back("word1-word2");

    w.push_back("word3-word4");

    w.push_back("word5-word6");

 

    cout << join_string(select_many(w,bind(split_string,_1,("-"))),":") << endl;

Почему "без проблем" в кавычках...?
Потому что сначала я попытался достигнуть результата используя только std::for_each и bind. Не получилось. Пришлось написать (ну помимо join_string и split_string функцию select_many.

template<class _Container,class _Fn1> inline

_Container select_many(const _Container& container, _Fn1& _Func)

{   

    _Container res;

    for(_Container::const_iterator v = container.begin();v!=container.end();++v)

    {

        _Container tmp = _Func(*v);

        for(_Container::const_iterator v1 = tmp.begin();v1!=tmp.end();++v1)

            res.push_back(*v1);

    }

    return res;

}



В принципе по своей сути она похожа на аналогичные в C# и Python, но увы - фактически до них ей ну очень далеко. Разница огромна, даже расписывать не буду.

К чему это я...

Если бы у меня была возможность выбрать какую возможность добавить в следующую версию С++ я бы пожалуй выбрал питоновские генераторы (они же шарповые IEnumerable).

среда, 27 февраля 2008 г.

Visual Studio Gallery

Visual Studio Gallery - сборник расширений и дополнений для Visual Studio

производительность STL

По наводке - STL vector performance

Выводы неутешительны - std::vector в среднем в 30 (ТРИДЦАТЬ) раз медленнее чем работа с чистым массивом на C (ну либо голом C++ без STL).

Но это только на первый взгляд.

1. Тестирование для STL pre-alloc было не совсем правильным. Нужно использовать reserve.
2. Размер NUM_ITERATIONS имеет значение...
3. Способ реаллокации памяти имеет значение...

Я повторил эти тесты, добавил reserve в код для "STL pre-alloc". А также расширил код, протестировав boost::array и boost::scoped_array.

Также я протестировал для различных значений NUM_ITERATIONS (от 10000 до 150000 с шагом 10000).

Результат (только для Average, CPI)

STL re-alloc  - Average, CPI: 65
C re-alloc - Average, CPI: 62

STL pre-alloc - Average, CPI: 10
C pre-alloc - Average, CPI: 9
boost::array - Average, CPI: 2
boost::scoped_array - Average, CPI: 10


Забавным здесь является не только то что нет разницы между "STL pre-alloc", "C pre-alloc",boost::array,boost::scoped_array.

Нет заявленной разницы между "STL re-alloc" и "C re-alloc". Я удивился, поскольку предполагал что разница будет... Удивление прошло когда я начал _уменьшать_ значение NUM_ITERATIONS. При NUM_ITERATIONS равном 5000 и меньше возникла разница в 2 раза.

Откуда берется эта разница? Смотрим в реализацию и видим что код реалокации памяти на С++ всегда выделяет новый больший блок памяти, потом копирует туда старый блок. Как по мне это имеет смысл в свете возможности использования vector с не POD типами.

Когда я изменил код реалокации в "C re-alloc" на:

                // Match STL vector allocation algorithm

                int* new_work =  (int *)malloc((size + size / 2) * sizeof(int));

                for ( long c = 0; c < array_size; ++c ) new_work[c] = work[c];

                free(work);

                work = newwork;

                size = size + size / 2;

разница в производительности между "C re-alloc" и "STL re-alloc" исчезла навсегда.

Еще о забавном тестировании производительности - C plus plus:Modern C plus plus:Vectors

Читаем секцию Safe & Fast, много смеемся...
The std::vector version builds its array 256 times larger, and yet is roughly 60 times faster.

Код на "С" с которым они "сравнивали" производительность (фрагмент с реалокацией массива)

void append_to_array(long *&array, long &array_size, long what) {

        long *newarray = new long[array_size+1];

        for ( long i = 0; i < array_size; ++i ) newarray[i] = array[i];

        newarray[array_size] = what;

        delete[] array;

        array = newarray;

        ++array_size;

}



Вывод - если вы работаете с POD типами, хотите работать с динамическим массивом и вам нужна максимальная производительность - пишите на С напишите свою реализацию vector :). А можете просто использовать "reserve".

пятница, 22 февраля 2008 г.

Project Euler, problem 78

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

четверг, 21 февраля 2008 г.

А вот за что я не люблю пропагандистов ФП

Так это за некорректные сравнения.

Статья Making Haskell faster than C!

В статье приводится пример оптимизации кода на Haskell таким образом что его выполнение происходит быстрее чем выполнение кода написанного на С. Это было бы приятно и интересно...если бы исходный код на С и на Haskell был бы хоть примерно эквивалентен.

Задача простая - подсчет количества слов в файле. Слова - это части текста отделенные пробелами/табуляцией/переносом строк.

Вот исходный код на С:

int main() {

    int i = 0;

    int c, last_space = 1, this_space;

    while ((c = getchar()) != EOF) {

        this_space = isspace(c);

        if (last_space && !this_space)

            i++;

        last_space = this_space;

    }

    printf("%i\n", i);

    return 0;

}



Вот исходный код на Haskell:

main = print . length . words =<< getContents

С момощью оптимизатора автор добивается того чтобы код на Haskell выполнялся быстрее кода на С. Потрясающий результат, если бы не одно "но".

Программа на С сделана явно с одной целью - написать ее самым медленным способом. Как добиться того чтобы программа выполнялась максимально медленно? Найти самую медленную опрацию и испльзовать ее максимально часто. В данном случае самая медленная операция это чтение из файла/потока. Как использовать ее максимально часто? Читать посимвольно...

Это ясно и понятно большенству программистов на С\С++. Т.е. программисты на С\С++ видят что автор сделал совершенно некорректное сравнение. Сознательно сделав код на С максимально медленным. Автор жулик.... и неважно сделал он это сознательно или нет.

В результате вместо того чтобы заинтересовать программистов на С\С++ возможностями Haskell в частности и ФП в целом мы получаем отторжение.