Домашняя страница Оптимизация C# кода. Создание класса ObjectPool
Публикация
Отменить

Оптимизация C# кода. Создание класса ObjectPool

Поскольку в C# используется управляемая сборка мусора, то нет возможностей контролировать создание, размещение и удаление объектов. Однако часто мы работаем с большим количеством повторяющихся данных, создаем и уничтожаем одинаковые объекты. В таких случаях следует работать не с объектами в отдельности, а с их множеством.

Например, когда пользователь попросил отобразить список каких то элементов, то вы заполняете TreeView определенными данными. Затем пользователь просит отобразить другие элементы, TreeView очищается и заполняется заново. Потом еще и еще раз. При этом идет постоянное создание и уничтожение объектов. Это не эффективно с точки зрения производительности, т.к. выделение и освобождение памяти с конструированием сложных объектов - это долгая операция (ни считая сборки мусора, которая так же занимает время).

Значительно рациональнее было бы повторно использовать уже созданные объекты. Это позволит единожды выделить все необходимые ресурсы и работать с ними. Это решит проблемы, описанные ранее. Именно о том, как создать такой ObjectPool и пойдет речь в данной статье.

Рассмотрим сначала ожидаемый алгоритм работы с таким пулом:
  1. Мы запрашиваем у него новый объект. Он его возвращает из созданных ранее либо создает заново.
  2. Когда объект больше не нужен мы сообщаем пулу, что этот объект свободен. Теперь пул может вернуть данный объект, при запросе нового объекта.

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

Создание пула:

Для хранения данных воспользуемся следующими структурами:

1
2
3
4
5
6
7
8
9
10
11
public delegate Object    CreateInstanceDelegate();        // делегат, для создания объектов данного типа

// внутренняя структура, хранящая информацию о пуле одного типа
private class PoolData
{
    public CreateInstanceDelegate    instancer;
    public Object[]            array;            // пул объектов
    public Int32            useObjectsCount;    // сколько объектов в пуле используется
}

private Dictionary< Type, PoolData >    _pools;            // библиотека пулов разных типов

CreateInstanceDelegate - это делегат, создающий объект данного типа. Если не использовать делегат, а использовать код, который создает новый объект по типу, то производительность падает в 2 раза. Поэтому новый объект создается при помощи делегата. Это позволит использовать не только конструктор по умолчанию для создания нового объекта, т.к. в делегате может быть какая либо дополнительная инициализация объекта.

В пуле объекты хранятся следующим образом: используемые объекты хранятся по порядку, за ними хранятся ссылки на объекты, которые не используются. Если объект еще не был создан, то хранится null. Когда объект освобождается, то он остается в памяти, но указатель на него переносится в секцию неиспользуемых объектов.

Перед тем, как объекты определенного типа могут хранится в пуле, этот тип нужно зарегистрировать. Кроме того, сразу указывается количество объектов, данного типа, которые нужно создать. Если максимальное количество элементов известно на этапе разработки, то стоит сразу создать все объекты. Тогда они вообще не будут создаваться во время выполнения, что значительно улучшит скорость работы с пулом.

1
2
3
4
5
6
7
8
9
10
11
12
public void    RegisterType( Type type, Int32 count, Int32 objectToCreate, CreateInstanceDelegate createDelegate )
{
    PoolData pool        = new PoolData();
    pool.array        = new Object[ count ];
    pool.instancer        = createDelegate;
    pool.useObjectsCount    = 0;
    _pools[ type ] = pool;

    // создание объектов данного типа
    for( Int32 idx = 0; idx < objectToCreate; ++idx )
        pool.array[ idx ] = pool.instancer();
}
  • type - регистрируемый тип
  • count - предполагаемое количество элементов
  • objectToCreate - количество объектов, которые будут сразу созданы, для оптимизации времени работы с пулом
  • createDelegate - делегат создания нового элемента

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object    GetObject( Type type )
{
    PoolData pool = _pools[ type ];

    //
    Object obj = null;
    lock( this )
    {
        // проверка размера массива. В релизе ее может не быть, для ускорения производительности
        if( pool.useObjectsCount == pool.array.Length )
            Array.Resize( ref pool.array, pool.array.Length * 2 );

        // берем не используемый элемент и создаем новый, если он еще не создан
        obj = pool.array[ pool.useObjectsCount ];
        if( null == obj )
        {
            obj = pool.instancer();
            pool.array[ pool.useObjectsCount ] = obj;
        }
        ++pool.useObjectsCount;
    }
    return obj;
}

После работы, объекты нужно удалить. В действительности система добавляет объекты в список свободных и сохраняет в памяти. Для указания того, что объект освобожден добавим функции FreeObject и FreeAll. ObjectPool очень хорошо подходит для использования именно функции FreeAll. Эта операция очень быстрая и должна использоваться всегда, где это только возможно. Функция FreeObject будет удалять только один объект. Это хорошо работает для небольшого числа объектов, но катастрофически долгая для большого числа. В действительности эта функция вряд ли будет использоваться (смотри тестирование).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void        FreeObject( Object obj )
{
    lock( this )
    {
        PoolData pool = _pools[ obj.GetType() ];
        Int32 idx = Array.IndexOf( pool.array, obj );
        if( pool.useObjectsCount > 0 && idx != -1 )
            pool.array[ idx ] = pool.array[ pool.useObjectsCount - 1 ];
        --pool.useObjectsCount;
    }
}

public void        FreeAll( Type type )
{
    lock( this )
    {
        _pools[ type ].useObjectsCount = 0;
    }
}

Вот публичный интерфейс класса ObjectPool, описанного в статье. В тексте методов я опустил некоторые проверки и ассерты, чтобы не перегружать статью. Их вы можете добавить сами. Теперь этот класс готов к работе. Я его применяю для оптимизации работы с интерфейсом, когда в зависимости от данных, интерфейс может содержать различные компоненты (например TreeView). Теперь нет необходимости их постоянно создавать заново, что значительно ускоряет работу.

1
2
3
4
5
6
7
8
9
10
public class ObjectPool
{
    public delegate Object        CreateInstanceDelegate();

    public             ObjectPool();
    public void        RegisterType( Type type, Int32 count, Int32 objectToCreate, CreateInstanceDelegate createDelegate );
    public Object        GetObject( Type type );
    public void        FreeObject( Object obj );
    public void        FreeAll( Type type );
}

Тестирование:

Рассмотрим именно такой сценарий, который описывался во вступлении. Допустим у вас есть некий интерфейс, для которого нужно постоянно создавать и уничтожать элементы. Причем создавать сразу много объектов, какого то типа, а потом их разом удалять. В примере будет использоваться тип ToolStripButton. Можно использовать тип TreeNode, смысл от этого не меняется.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Int32 N = 250000, t1 = 0, t2 = 0;
List< ToolStripButton > list1 = new List< ToolStripButton >( N );
List< ToolStripButton > list2 = new List< ToolStripButton >( N );
List< ToolStripButton > list3 = new List< ToolStripButton >( N );
List< ToolStripButton > list6 = new List< ToolStripButton >( N );

// создаем два пула, для разных тестов
ObjectPool pool1 = new ObjectPool();
pool1.RegisterType( typeof( ToolStripButton ), N * 2, 0, delegate(){ return new ToolStripButton(); } );
ObjectPool pool2 = new ObjectPool();

//
t1 = Environment.TickCount;
for( int i = 0; i < N; ++i )
    list1.Add( new ToolStripButton() );
t2 = Environment.TickCount;
Console.WriteLine( "Создание объектов операцией new: " + (t2 - t1).ToString() );

//
t1 = Environment.TickCount;
for( int i = 0; i < N; ++i )
    list2.Add( pool1.GetObject( typeof( ToolStripButton ) ) as ToolStripButton );
t2 = Environment.TickCount;
Console.WriteLine( "Создание объектов в пуле: " + (t2 - t1).ToString() );

//
t1 = Environment.TickCount;
pool2.RegisterType( typeof( ToolStripButton ), N, N, delegate(){ return new ToolStripButton(); } );
t2 = Environment.TickCount;
Console.WriteLine( "Резервирование объектов: " + (t2 - t1).ToString() );

//
t1 = Environment.TickCount;
for( int i = 0; i < N; ++i )
    list3.Add( pool2.GetObject( typeof( ToolStripButton ) ) as ToolStripButton );
t2 = Environment.TickCount;
Console.WriteLine( "Взятие объектов из пула: " + (t2 - t1).ToString() );

//
t1 = Environment.TickCount;
for( int i = 0; i < N; ++i )
    pool2.FreeObject( list6[ i ] );
t2 = Environment.TickCount;
Console.WriteLine( "По объектное освобождение: " + (t2 - t1).ToString() );

В результате мы получаем такие результаты при компиляции в релизе (цифры в миллисекундах):

1
2
3
4
5
Создание объектов операцией new: 546
Создание объектов в пуле: 765
Резервирование объектов: 592
Взятие объектов из пула: 16
По объектное освобождение: 81750

Из результатов мы видим, что если использовать не создавать предварительно объекты, а запрашивать их сразу у пула, то время на получение нового объекта увеличивается примерно на 40%. Причем это только при получение объектов, когда в пуле нет свободных. Если же в пуле они есть (заготовленные заранее или освобожденные), то работа с пулом идет в 34 раза быстрее для объекта ToolStripButton. Очевидно, что выигрыш в производительности будет тем больше, чем сложнее класс. В случае создания меняющегося графического интерфейса, наличие такого пула позволит кардинально ускорить работу интерфейса.

Из тестов видно, что поэлементное удаление занимает очень большое время (тестировался самый худший случай.. при обратном порядке удаление элементов, тест пройдет мгновенно). На столько большое, что эта возможность становится практически бесполезной. Она может быть использована только с объектами, которых мало и которые очень большие (занимают много памяти и долго инициализируются). В этом случае выигрыш будет достигаться отсутствием постоянных созданий новых объектов.

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

Читайте другие статьи по оптимизации кода в C#:

Комментарии

FiloXSee, 25 сент. 2011, 13:55

В некоторых комментариях мне предлагалось заменить Listна Object[] или даже сделать пул на шаблонах. После тестирования я решил отказаться от этих изменений. Дело в том, что в основе List<> лежит именно массив. Добавление в конец или удаление последнего элемента выполняются за константное время, при условии, что размер массива достаточный. Учитывая что при регистрации типа резервируется место под определенное количество элементов, то если в пуле элементов не станет больше, то работа с типом List<> будет такой же быстрой, как и с Object[] или T[].

Публикация защищена лицензией CC BY 4.0 .

Оптимизация C# кода. Работа с ресурсами сборки.

Каждый год площадь сельскохозяйственных земель уменьшается на одну Италию