Принципы объектно-ориентированного программирования

         

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



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

Производительность связана с проблемой завершения, и поэтому является важным аспектом. Команда разработчиков .NET считает, что должен существовать некоторый трассирующий сборщик, задача которого — обработка циклов (на которую тратится большая часть ресурсов, потребляемых сборщиком). Также считается, что расход ресурсов, связанный с работой счетчика ссылок, может серьезно повлиять на производительность исполнения кода. К счастью, в контексте всех объектов, выделенных работающей программой, число объектов, которым действительно требуется детерминированное завершение, невелико. Однако обычно нелегко изолировать размер расхода ресурсов исключительно данным объектом или набором объектов. Рассмотрим фрагмент псевдокода, выполняющего простое присваивание ссылки при использовании трассирующего сборщика, а затем псевдокод, применяющий счетчик ссылок:

// Трассировка, а = Ь;

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

// Счетчик ссылок, if (a != null)



if (InterlockedDecrement(ref a.m_ref) == 0) a. FinalReleaseO;

if (b != null)

Interlockedlncrement(ref b.m_ref);

a = b;

Этот код сильно раздут, его рабочий набор больше, и производительность исполнения неприлично мала, особенно при двух взаимоблокирующих командах. Разбухание кода можно ограничить, поместив эти вещи во вспомогательный метод, еще больше увеличив при этом вложенность кода. Кроме того, когда вы разместите все необходимые блоки try, пострадает генерация кода, так как в силу некоторых причин из-за присутствия кода обработки исключений у средства оптимизации кода будут "связаны руки". Это верно и для C++, где нет такого управления ресурсами. Стоит отметить и то, что размер каждого объекта при этом возрастает на 4 байта из-за дополнительного поля счетчика ссылок, снова увеличивая использование памяти.

Два примера, любезно предоставленные командой разработчиков .NET, демонстрируют связанный с этим расход ресурсов. Они выполняют тестовые циклы, выделение объектов, два присвоения и выводят за пределы видимости одну из ссылок. Результаты работы этих приложений, как и результаты любых тестов, можно интерпретировать субъективно. Можно приводить даже такие аргументы, что в контексте этой программы большая часть операций по подсчету ссылок может быть опущена в результате оптимизации. Может, это и так, но нам нужно лишь продемонстрировать сам эффект. В настоящей программе выполнить оптимизацию подобного рода трудно, если вообще возможно. Фактически программисты на C++ делают такую оптимизацию вручную, что ведет к возникновению ошибок при подсчете ссылок. Поэтому в настоящих программах отношение "присвоение/выделение памяти" много выше, чем в приведенных здесь примерах.

Вот первый пример, ref^gc.cs. Эта версия использует трассирующий GC:

using System;

public class Foo {

private static Foo m_f;

private int mjnember;

public static void Main(String[] args)

{

int ticks = Environment.TickCount;

Foo f2 = null;

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

Foo f = new FooQ;

// Присваивание f2 значения статического объекта. f2 = m_f;

// Присваивание f статическому объекту. m_f = f;

// f выходит за пределы видимости. }

// Присваивание f2 статическому объекту. m_f = f2;

// f2 выходит за пределы видимости.

/

/ / ticks = Environment.TickCount - ticks;

/

Console.WriteLine("Ticks = {0}", ticks);

>

public Foo() { } }

А вот и второй пример, refjm.cs — версия, использующая подсчет ссылок с помощью взаимоблокирующих операций для защиты потоков:

using System;

using System.Threading;

public class Foo {

private static Foo m_f;

private int mjnember;

private int m_ref;

public static void Main(String[] args) <

int ticks = Environment.TickCount;

Foo f2 = null;

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

Foo f = new Foo();

// Присваивание f2 значения статического объекта.

if (f2 != null)

{

if (Interlocked.Decrement(ref f2.m_ref) == 0)

f2.Dispose(); } if (m_f != null)

Interlocked.Increment(ref m_f.m_ref); f2 = m_f;

// Присваивание f статическому объекту.

if (m_f != null)

{

if (Interlocked.Decrement(ref m_f.m_ref) == 0)

m_f. DisposeO; > if (f != null)

Interlocked.Increment(ref f.m_ref); m_f = f;

// f выходит за пределы видимости, if (Interlocked.Decrement(ref f.m_ref) == 0) f. DisposeO;

}

// Присваивание f2 статическому объекту, if (m_f != null)

{

if (Interlocked.Decrement(ref m_f.m_ref) == 0)

m_f. DisposeO; } if (f2 != null)

Interlocked.Increment(ref f2.m_ref); m_f = f2;

// f2 выходит за пределы видимости, if (Interlocked.Decrement(ref f2.m_ref) == 0) f 2. DisposeO;

ticks = Environment.TickCount - ticks; Console.WriteLine("Ticks = {0}", ticks); }

public Foo() {

m_ref = 1; }

public virtual void DisposeO

{

}

}

Здесь присутствует лишь один поток, и нет конкуренции за ресурсы шины, чуо делает рассматриваемый случай "идеальным". Вероятно, вы сможете'отрегулировать программу лучше, но ненамного. Замечу также, что Visual Basic исторически не должен был беспокоиться о применении взаимоблокирующих операций для подсчета ссылок (хотя Visual C++ — должен). В Прежних выпусках Visual Basic компонент запускался в од-нопоточном окружении, гарантирующем исполнение лишь одного потока в один момент времени. Одна из задач Visual Basic.NET — поддержка многопоточного программирования, а другая — избавление от сложности, присущей моделям многопоточности СОМ. Но тем из вас, кому требуется версия, не использующая префиксы блокировки, будет интересен следующий пример, также предоставленный командой разработчиков .NET. ref_rs.cs, — версия, применяющая счетчик ссылок, которая "считает", что она работает в однопоточном окружении. Эта программа работает не так медленно, как многопоточная версия, но все же медленнее версии, использующей GC.

using System;

public class Foo {

private static Foo m_f;

private int mjnember;

private int m_ref;

public static void Main(String[] args) {

int ticks = Environment.TickCount;

Foo f2 = null;

for (int 1=0; i < 10000000; ++i) {

Foo f = new Foo();

// Присваивание f статическому объекту.

if (f2 != null)

{

if (-f2.m_ref == 0)

f2.Dispose(); } if (m_f l= null)

++m_f.m_ref; f2 = m_f;

// Присваивание f статическому объекту.

if (m_f l= null)

{

if (-m_f.m_ref == 0)

m_f .DisposeO; } if (f l= null)

++f.m_ref; m_f = f;

// f выходит за пределы видимости, if (-f.m_ref == 0)

f. DisposeO; }

// Присваивание f2 статическому объекту.

if (m_f != null)

{

if (-m_f.m_ref == 0)

m_f .DisposeO; } if (f2 != null)

++f2.m_ref; m_f = f2;

// f2 выходит за пределы видимости, if (-f2.m_ref == 0) f 2. DisposeO;

ticks = Environment.TickCount - ticks; Console.WriteLine("Ticks = {0}", ticks); >

public Foo() {

m_ref = 1;

} "~""^^\

"x

public virtual void DisposeO \ч

{ X > }

Видно, что переменных здесь хватает. В результате запуска всех трех приложений, можно отметить, что версия, использующая GC, работала практически вдвое быстрее однопоточной версии, использующей счетчик ссылок, и вчетверо — версии, использующей счетчик ссылок и префиксы блокировки. Лично у меня получились такие результаты (заметьте: числа представляют собой средние значения, полученные на IBM ThinkPad 570 с помощью компилятора .NET Framework SDK Beta 1):

GC Version (ref_gc) 1162ms

Ref Counting (ref_rm)(raulti-threaded) 4757ms

flef Counting (ref_rs)(single-threaded) 1913ms

 

Содержание раздела