вторник, 22 мая 2007 г.

Эффективные коллекции

Несколько дней назад у меня появилась необходимость максимально эффективного доступа к элементам, которые хранятся в объекте-хранилище. Все элементы имеют одного предка, определяющего их общие признаки и идентифицирующий каждый элемент уникальным целочисленным идентификатором. Каждый тип элементов хранится в отдельной коллекции. Для хранения элементов в объекте-хранилище я использовал обобщенную коллекцию List, по одной на каждый тип. Мне был необходим метод, позволявший получать элементы из хранилища по типу и идентификатору. Этот метод не должен быть обобщенным, так как должен вызывается через делегат из метода, который имеет только лишь Type запрашиваемого элемента. Таким образом, эффективность применения List сводилась на нет, процесс получения элемента работал очень медленно. На одном из форумов RSDN мне подсказали решение основанное на KeyedCollection. Оно оказалось настолько эффективным для моей задачи, что я был просто поражен. Результаты сравнения решений с использованием List и KeyedCollection, которые приведены здесь, показывают превосходство второго способа более чем в 500 раз. Думаю, это больше чем хорошая мотивация, что бы присмотреться к данному решению. Кстати, в процессе тестирования я помимо этих двух вариантов добавил вариант с использованием ArrayList, для того что бы показать что использование List ничем не отличается по эффективности от использования необобщенной коллекции. Итак, приступим к описанию всех решений.

Иерархия элементов для тестирования эффективности:


public class base1
{
public int i;
public base1(int iq) { i = iq; }
};

public class der1 : base1
{
public string s1;
public der1(int iq, string s) : base(iq) { s1 = s; }
};

public class der2 : base1
{
public string s2;
public der2(int iq, string s) : base(iq) { s2 = s; }
};

Абстрактный класс, описывающий произвольный объект-хранилище для хранения элементов и выполнения требуемых для данной задачи операций:


public abstract class BaseRep
{
// инициализация репозитория
public abstract void Init(Type[] ar);

// добавление элемента
public abstract void Add<T>(T item) where T : base1;

// выборка элемента
public abstract base1 GetCollectionItem(Type collectionType,
int input);

// используется для вывода названия типа
// репозитория в процессе выполнения теста
public string GetName() { return this.GetType().Name; }
}

Реализация объекта-хранилища с использованием KeyedCollection:


// интерфейс для быстрого доступа к элементам хранилища
public interface IIndexable<TKey, TItem>
{
TItem this[TKey key] { get; }
}

// реализация коллекции, используемой для хранения элементов
public class myCollection<T> : KeyedCollection<int, T>,
IIndexable<int, base1> where T : base1
{
// переопределение абстрактного метода KeyedCollection
protected override int GetKeyForItem(T item)
{
return item.i;
}

// реализация метода интерфейса
base1 IIndexable<int, base1>.this[int key]
{
get
{
base1 value = null;
if (Contains(key))
value = this[key];
return value;
}
}
}

// реализация объекта-хранилища
public class keyedRep : BaseRep
{
private Dictionary<Type, IIndexable<int, base1>> array =
new Dictionary<Type, IIndexable<int, base1>>();

public override void Init(Type[] ar)
{
foreach (Type type in ar)
{
Type genType = typeof(myCollection<>).MakeGenericType(type);
array.Add(type, (IIndexable<int,
base1>)Activator.CreateInstance(genType));
}
}

public override void Add<T>(T item)
{
((myCollection<T>)array[typeof(T)]).Add(item);
}

public override base1 GetCollectionItem(Type collectionType,
int input)
{
return array[collectionType][input];
}
};

Более простая реализация объекта-хранилища с использованием List:


public class listRep : BaseRep
{
Dictionary<Type, object> array = new Dictionary<Type, object>();

public override void Init(Type[] ar)
{
foreach (Type type in ar)
{
Type genType = typeof(List<>).MakeGenericType(new Type[] { type });
array.Add(type, Activator.CreateInstance(genType));
}
}

public override void Add<T>(T item)
{
((List<T>)array[typeof(T)]).Add(item);
}

public override base1 GetCollectionItem(Type collectionType, int input)
{
if (array.ContainsKey(collectionType))
{
IEnumerable collection = (IEnumerable)array[collectionType];
foreach (base1 item in collection)
{
if (item.i == input)
return item;
}
}
return null;
}
}

Реализация объекта-хранилища практически аналогичная предыдущей, за исключением того, что вместо List используется ArrayList:


public class arrayListRep : BaseRep
{
Dictionary<Type, ArrayList> array = new Dictionary<Type, ArrayList>();

public override void Init(Type[] ar)
{
foreach (Type type in ar)
{
array.Add(type, (ArrayList)Activator.CreateInstance(typeof(ArrayList)));
}
}

public override void Add<T>(T item)
{
((ArrayList)array[typeof(T)]).Add(item);
}

public override base1 GetCollectionItem(Type collectionType, int input)
{
if (array.ContainsKey(collectionType))
{
ArrayList collection = array[collectionType];
foreach (base1 item in collection)
{
if (item.i == input)
return item;
}
}
return null;
}
}

И наконец, метод осуществляющий тестирование всех трех вариантов:


private static void Test()
{
// количество элементов каждого типа и количество итераций при выборке
int iterCount = 10000;

// типы элементов, которые мы будем использовать
Type[] types = new Type[] { typeof(der1), typeof(der2) };

// формируем списки каждого типа элементов,
// которые позже будем использовать при добавлении в репозиторий
List<der1> d1 = new List<der1>();
List<der2> d2 = new List<der2>();
for (int i = 0; i < iterCount; i++)
{
d1.Add(new der1(i, "test"));
d2.Add(new der2(i, "test"));
}

// эмуляция произвольного доступа, определяем индексы элементов
// которые будем получать в процессе выборки
//(выборка должна быть одинаковая для всех, что бы никому не было обидно;))
List<int> accessInd = new List<int>();
Random rnd = new Random();
for (int i = 0; i < iterCount; i++)
{
accessInd.Add(rnd.Next(iterCount - 1));
}

//формируем список репозиториев для тестирования
BaseRep[] reps = { new listRep(), new arrayListRep(), new keyedRep() };

//
Stopwatch sw = new Stopwatch();

// для каждого репозитория проводим добавление и выборку
// с подсчетом затраченного времени на каждую операцию
foreach (BaseRep rep in reps)
{
// инициализация
rep.Init(types);

sw.Start();

// заполнение
for (int i = 0; i < iterCount; i++)
{
rep.Add(d1[i]);
rep.Add(d2[i]);
}
sw.Stop();

// результаты
System.Console.WriteLine("{0} adding: {1} ticks or {2} ms",
rep.GetName(), sw.ElapsedTicks, sw.ElapsedMilliseconds);
sw.Reset();

sw.Start();

// выборка
for (int i = 0; i < accessInd.Count; i++)
{
// попеременно запрашиваем элементы различных типов
base1 b = rep.GetCollectionItem(types[i % 2], accessInd[i]);
}
sw.Stop();

// результаты
System.Console.WriteLine("{0} reading: {1} ticks or {2} ms",
rep.GetName(), sw.ElapsedTicks, sw.ElapsedMilliseconds);
sw.Reset();

System.Console.WriteLine(Environment.NewLine);
}
}

Таким образом, вот какие я получил результаты на своем компьютере:

Результаты (MSVS2k5, Release, WinXP, Athlon64 3000+):

listRep adding: 25441 ticks or 7 ms
listRep reading: 8778434 ticks or 2452 ms


arrayListRep adding: 24542 ticks or 6 ms
arrayListRep reading: 9088774 ticks or 2539 ms


keyedRep adding: 68186 ticks or 19 ms
keyedRep reading: 19248 ticks or 5 ms


Решения с List и ArrayList показили себя практически одинаково малоэффективно.
Однако, добавление в такие коллекции осуществляется в 3 раза быстрее, чем в случае с KeyedCollection. Оно и понятно — в линейных коллекциях добавление не требует каких-либо чрезмерных усилий и дополнительных затрат.
А вот резульаты произвольной выборки — это что-то... 5 мс против ~2500 мс, то есть практически в 500 раз KeyedCollection работает эффективней.
В виду того на практике в большинстве случаев выборка или получение элементов из хранилища используется гораздо чаще, чем операция добавления (удаления, изменения), то фактом более медленного добавления в принципе можно пренебречь. Таким образом, если у Вас возникнет ситуация похожая на ту, которая опианная здесь, Вы уже знаете что делать

суббота, 12 мая 2007 г.

Введение в функциональные языки программирования

Большинство языков программирования, повсеместно используемых для разработки программного обеспечения, использует императивную парадигму программирования. Это значит, что процесс программирования представляет собой перечень инструкций или команд, которые изменяют состояние программы. Императивный подход имеет много общего с естественными языками, поэтому он достаточно понятен для понимания, и языки программирования базирующиеся на нем легче поддаются изучению.
В противовес императивной парадигме как правило противопоставляют декларативный подход. Суть подхода состоит в том, что программа представляет собой не набор команд, а описание действий, которое не предполагает такой строгой последовательности и ориентировано прежде всего на описание желаемого результата. Такой подход существенно проще и прозрачнее формализуется математическими средствами и, как следствие требует для понимания гораздо более высокую математическую подготовку. Одним из путей развития декларативной парадигмы стал функциональный подход к программированию.
Функциональный подход базируется на стиле программирования, при использовании которого в программах описывается способ решения поставленной задачи посредством вычисления значений функций с одним или несколькими аргументами, которые также можно рассматривать как функции. Любая функция представляет собой конструкцию языка, которая описывает правила преобразования своего аргумента в результат. Функцию, принимающую в качестве аргументов другие функции или возвращающую другую функцию в качестве результата, часто называют функцией высшего порядка. С теоретической точки зрения любая функция при функциональном подходе является функцией высшего порядка.
Благодаря тому что выражения в функциональном программировании очень схожи с выражениями в естественной математике, программы написанные на функциональных языках в большинстве случаев короче и нагляднее, чем те же самые программы на традиционных императивных языках. Кроме того, данная особенность достигается еще и посредством того, что функциональные языки являются более абстрактными по отношению к особенностям вычислительной техники, и для решения задачи используют подходы максимально приближенные к математическим вычислениям. Функциональные программы не содержат операторов присваивания, а переменные, получив однажды значение, никогда не изменяются. Именно по этой причине все операции с памятью выполняются автоматически.
Как правило каждому выражению (константа, переменная, функция) функционального языка поставлен в соответствие тот или иной тип. Такая система типизации в языках функционального программирования называется системой сильной типизации, а сам язык - языком с сильной типизацией. При этом многие функциональные языки имеют механизм выводимости типов (type inference), который позволяет определять типы констант, выражений и функций из контекста. То есть, если не задан тип переменной, входящей в состав выражения, то он может быть автоматически выведен на основании типов других переменных, которые входят в состав данного выражения. По этой причине типы используемых функций в некоторых случаях можно не указывать. Механизм выводимости типов не является исключительной особенностью функциональных языков, он так же существует и в некоторых современных императивных языках, однако впервые подобная возможность была реализована именно в функциональных языках.
В чистом функциональном программировании оператор присваивания отсутствует, а используемые переменные, являются просто псевдонимами для выражений. Именно по этой причине они не могут быть изменены или уничтожены. Благодаря этой особенности все функции свободны от побочных эффектов. Кроме того независимость и произвольный порядок выполнения функций делает функциональный подход удобным для параллельных вычислений.
Еще одной особенностью чистых функциональных языков является наличие механизма отложенного вычисления. В этом случае аргумент вычисляется, только если он нужен для вычисления результата. Такой поход иначе известен как «ленивые» вычисления, и у некоторых функциональных языков присутствует даже специальное ключевое слово lazy, которое описывает выражение, вычисляемое по необходимости. Следует отметить что механизм отложенного вычисления реализован не во всех функциональных языках.
Программы реализованные с помощью функциональных языков как правило работают медленнее чем аналоги, использующие императивный подход, однако они могут решать более абстрактные задачи с меньшими трудозатратами и являются весьма эффективными при реализации символьной обработки и анализе текстов. Помимо этого функциональный подход дает возможность прозрачного моделирования текста программ математическими средствами поэтому он активно используется в академических целях.
Впервые функциональный подход подход был воплощен в языке LISP. Основной задачей данного языка является символьная обработка данных. В своей основе LISP - это язык списков. Все в LISP, от данных до кода приложения, является списком. Хотя LISP не является строго функциональным языком, так как существующие в нем императивные свойства лишают его права так называться в строгом смысле слова, многие идиомы и функциональные возможности LISP имеют сильный функциональный привкус. Этот язык дал развитие большому количеству различных диалектов, наиболее распространенными из которых являются Common Lisp и Scheme, которые очень активно используются в наши дни.
Дальнейшим развитием функционального программирования стало появление семейства языков ML (Meta Language). Этот язык был задуман как инструмент для научных и образовательных учреждений, предназначенный для построения формальных доказательств в системе логики для вычислимых функций. Однако возможность практического применения языка для промышленных целей, к примеру для символьных вычислений спровоцировало пересмотр его концепции, что явилось причиной появления различных диалектов, таких как Standard ML, CaML Light и Objective CaML. К сожалению данное семейство также не является чисто функциональным.
Первым чистым функциональным языком стал язык SASL (сокр. от St. Andrews Static Language) в далеком 1972 году, который позже перерос в KRC (Kent Recursive Calculator). В свою очередь KRC дал развитие новому языку – Miranda, который используется и в наше время. Язык Miranda разрабатывался в качестве стандартного функционального языка. Сейчас он преподаётся во многих университетах, и так же как и ML и LISP оказал большое влияние на развитие функционального подхода.
В наше время к чистым функциональным языкам программирования, которые получили развитие и используются не только в академических целях можно отнести Haskell и Clean. Haskell является наиболее распространённым чисто функциональным языком программирования. Последний стандарт языка, по праву стал стандартом функционального программирования — Haskell-98. Язык Clean намного меньше распространен, чем Haskell, однако его синтаксис несильно отличается от синтаксиса Haskell. Главное отличие этих языков заключается в способе вычислений выражений.
Помимо всего этого функциональный подход получил огромное развитие в мультипарадигменных языках, сочетающих в себе различные подходы, и языках не являющихся чисто функциональными. Так к примеру Erlang - язык сочетает в себе функциональную сущность и понятие процессов. Идеологие данного языка является выражение "всё есть процесс" Он активно используется для разработок разного рода распределённых систем. Язык Scala сочетает возможности функционального и объектно ориентированного программирования, и имеет реализацию для платформы JVM. Nemerle является высокоуровневым компилируемым языком программирования для .NET и использует функциональный, объектно-ориентированный и императивный подходы. Python – язык, который по сути является объектно-ориентированным, использует помимо всего особенности и других парадигм, в том числе и функциональную. Python используется в различных качествах: как основной язык реализации, для создания расширений, для интеграции приложений. Подобно ему, существует большое количество различных языков, использующих в той или иной мере функциональный подход.
Функциональный подход это не альтернатива императивному. Это просто другой взгляд на теорию программирования, со своей культурой, теорией и спецификой, который обладает как преимуществами так и недостатками, присущими ему. Поэтому нет смысла сравнивать эти подходы или противопоставлять, гораздо лучше их совместить во благо разрабатываемых всеми нами программных проектов;)

Литература, использованная при написании статьи:

Функциональное программирование для всех
Курс введение в теорию программирования. Функциональный подход
Курс основы функционального программирования
Основы функционального программирования
Лекции по функциональному программированию
Декларативное программирование
Сильные стороны функционального программирования
Описание языка Haskell
Мягкое введение в Haskell. Часть 1
Форумы RSDN
Википедия