Поделиться через


Вопросы производительности для технологий Run-Time в платформа .NET Framework

 

Эммануэль Шанцер
Microsoft Corporation

Август 2001 г.

Сводка: Эта статья содержит обзор различных технологий, работающих в управляемом мире, и техническое объяснение того, как они влияют на производительность. Узнайте о работе сборки мусора, JIT,удаленного взаимодействия, ValueTypes, безопасности и т. д. (27 печатных страниц)

Содержимое

Общие сведения
Сборка мусора
Пул потоков
The JIT
Домены приложений
Безопасность
Удаленное взаимодействие
ValueTypes
Дополнительные ресурсы
Приложение. Размещение времени выполнения сервера

Общие сведения

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

Этот технический документ должен дать вам более глубокое представление о технологиях, работающих в .NET, и поможет вам настроить код для быстрой настройки. Примечание. Это не спецификация. Существует много твердой технической информации там уже. Цель здесь заключается в том, чтобы предоставить информацию с сильным наклоном к производительности и может не ответить на все технические вопросы, которые у вас есть. Если вы не найдете нужных ответов, рекомендуется искать дополнительные сведения в веб-библиотеке MSDN.

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

Сборка мусора

Основы

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

Foo a = new Foo();      // Allocate memory for the object and initialize
...a...                  // Use the object   
delete a;               // Tear down the state of the object, clean up
                        // and free the memory for that object

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

Foo a = new Foo();      // Allocate memory for the object and initialize
...a...                  // Use the object (it is strongly reachable)
a = null;               // A becomes unreachable (out of scope, nulled, etc)
                        // Eventually a collection occurs, and a's resources
                        // are torn down and the memory is freed

Пока объект не будет освобожден, в обоих мирах выполняются одни и те же действия. В машинном коде необходимо не забывать, чтобы освободить объект после завершения работы с ним. В управляемом коде, когда объект больше недоступен, сборка мусора может собрать его. Конечно, если ресурс требует особого внимания к освобождению (скажем, закрытию сокета), сборке мусора может потребоваться помощь, чтобы правильно закрыть его. Код, написанный ранее для очистки ресурса перед его освобождением, по-прежнему применяется в виде методов Dispose() и Finalize(). Я расскажу о различиях между этими двумя позже.

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

Сборщик мусора СРЕДЫ CLR является сборщиком с пометкой и компактом поколений. Он следует нескольким принципам, которые позволяют ему достичь отличной производительности. Во-первых, существует представление о том, что объекты, которые являются кратковременными, как правило, меньше и к которым часто обращаются. Сборка мусора разделяет граф распределения на несколько вложенных графов, называемых поколениями, которые позволяют ему тратить как можно меньше времени на сбор*.* Поколение 0 содержит молодые, часто используемые объекты. Это также, как правило, самый маленький, и требуется около 10 миллисекунд для сбора. Так как сборка мусора может игнорировать другие поколения во время этой коллекции, она обеспечивает гораздо более высокую производительность. G1 и G2 предназначены для больших и старых объектов и собираются реже. При возникновении коллекции G1 также собирается G0. Коллекция G2 — это полная коллекция, и это единственный раз, когда сборка мусора проходит весь граф. Он также интеллектуально использует кэши ЦП, которые могут настроить подсистему памяти для конкретного процессора, на котором она выполняется. Эта оптимизация не легко доступна в собственном выделении и может помочь приложению повысить производительность.

Когда происходит коллекция?

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

Что происходит при создании коллекции?

Давайте рассмотрим шаги, которые выполняет сборщик мусора во время сбора. Сборка мусора поддерживает список корней, которые указывают на кучу сборки мусора. Если объект является активным, в куче имеется корень для его расположения. Объекты в куче также могут указывать друг на друга. Этот график указателей является тем, что сборка мусора должна искать, чтобы освободить место. Порядок событий выглядит следующим образом:

  1. Управляемая куча сохраняет все свое пространство выделения в непрерывном блоке, и если этот блок меньше запрошенной суммы, вызывается сборка мусора.

  2. Сборка мусора следует за каждым корнем и всеми указателями, которые следуют за ней, сохраняя список объектов, которые недоступны .

  3. Каждый объект, недоступный из любого корня, считается коллекционируемым и помечается для коллекции.

    Рис. 1. Перед сбором. Обратите внимание, что не все блоки доступны из корней!

  4. Удаление объектов из графа доступности делает большинство объектов пригодными для сбора. Однако некоторые ресурсы необходимо обрабатывать специально. При определении объекта можно написать метод Dispose() или Finalize() (или и то, и другое). Я расскажу о различиях между ними, и когда их использовать позже.

  5. Последним этапом в коллекции является этап сжатия. Все используемые объекты перемещаются в непрерывный блок, а все указатели и корни обновляются.

  6. Сжимая динамические объекты и обновляя начальный адрес свободного пространства, сборка мусора утверждает, что все свободное пространство является непрерывным. Если для выделения объекта достаточно места, сборка мусора возвращает программе управление. В противном случае возникает OutOfMemoryExceptionисключение .

    Рис. 2. После сбора: доступные блоки сжаты. Больше свободного места!

Дополнительные технические сведения об управлении памятью см. в главе 3 статьи Программирование приложений для Microsoft Windows джеффри Рихтера (Microsoft Press, 1999).

Очистка объектов

Некоторые объекты требуют специальной обработки, прежде чем их ресурсы могут быть возвращены. Примерами таких ресурсов являются файлы, сетевые сокеты или подключения к базе данных. Просто освободить память в куче будет недостаточно, так как вы хотите, чтобы эти ресурсы были корректно закрыты. Для очистки объектов можно написать метод Dispose(),Метод Finalize() или и то, и другое.

Метод Finalize():

  • Вызывается сборкой мусора
  • Не гарантируется вызов в любом порядке или в прогнозируемое время
  • После вызова освобождает память после следующей сборки мусора.
  • Сохраняет все дочерние объекты до следующей сборки мусора

Метод Dispose():

  • Вызывается программистом
  • Упорядочено и запланировано программистом
  • Возвращает ресурсы после завершения метода

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

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

  • В сценарии, когда у вас есть ресурсы, которые необходимо освободить в определенное время, вы теряете контроль над средствами завершения. Предположим, у вас есть открытый файл, и его необходимо закрыть из соображений безопасности. Даже если присвоить объекту значение NULL и немедленно принудить сборку мусора, файл будет оставаться открытым до вызова метода Finalize(), и вы не знаете, когда это может произойти.
  • N объектов, требующих удаления в определенном порядке, могут обрабатываться неправильно.
  • Огромный объект и его дочерние объекты могут занимать слишком много памяти, требовать дополнительных коллекций и повредить производительность. Эти объекты могут не собираться в течение длительного времени.
  • Небольшой объект для завершения может иметь указатели на большие ресурсы, которые можно освободить в любое время. Эти объекты не будут освобождены до тех пор, пока объект, который необходимо завершить, не будет обработан, что приведет к ненужной нехватке памяти и принудив к частым сборам данных.

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

Рис. 3. Пути удаления и завершения, которые может принимать объект

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

Выбор используемого сборщика мусора

Среда CLR имеет два разных GCs: Рабочая станция (mscorwks.dll) и Серверная (mscorsvr.dll). При работе в режиме рабочей станции задержка является большей проблемой, чем пространством или эффективностью. Сервер с несколькими процессорами и клиентами, подключенными по сети, может позволить себе некоторую задержку, но пропускная способность теперь является главным приоритетом. Вместо того чтобы объединить оба этих сценария в единую схему сборки мусора, корпорация Майкрософт включила два сборщика мусора, которые адаптированы к каждой ситуации.

Сборка мусора сервера:

  • Многопроцессорный (MP) Масштабируемый, параллельный
  • Один поток сборки мусора на ЦП
  • Программа приостановлена во время маркировки

Сборка рабочих станций:

  • Сводит к минимуму паузы путем параллельного выполнения во время полных коллекций

Серверная сборка мусора предназначена для обеспечения максимальной пропускной способности и масштабируется с очень высокой производительностью. Фрагментация памяти на серверах является гораздо более серьезной проблемой, чем на рабочих станциях, что делает сбор мусора привлекательным предложением. В сценарии с однопроцессором оба сборщика работают одинаково: в режиме рабочей станции без параллельной сборки. На компьютере с mp сборка мусора рабочей станции использует второй процессор для параллельного выполнения коллекции, минимизируя задержки и уменьшая пропускную способность. Серверная сборка мусора использует несколько кучи и потоков сбора, чтобы увеличить пропускную способность и улучшить масштабирование.

Вы можете выбрать, какую сборку мусора использовать при размещении времени выполнения. При загрузке времени выполнения в процесс указывается используемый сборщик. Загрузка API рассматривается в руководстве разработчика платформа .NET Framework. Пример простой программы, которая размещает время выполнения и выбирает сборку мусора сервера, см. в приложении.

Миф: сборка мусора всегда медленнее, чем делать это вручную

На самом деле, пока не будет вызвана коллекция, сборка мусора выполняется гораздо быстрее, чем сделать это вручную в C. Это удивляет многих людей, так что это стоит некоторых объяснений. Прежде всего, обратите внимание, что поиск свободного места происходит в течение постоянного времени. Так как все свободное пространство является непрерывным, сборка мусора просто следует указателю и проверяет, достаточно ли места. В C вызов malloc() обычно приводит к поиску связанного списка свободных блоков. Это может занять много времени, особенно если куча сильно фрагментирована. Что еще хуже, несколько реализаций времени выполнения C блокируют кучу во время этой процедуры. После выделения или использования памяти необходимо обновить список. В среде сборки мусора выделение осуществляется бесплатно, а память освобождается во время сбора. Более продвинутые программисты будут резервировать большие блоки памяти и самостоятельно обрабатывать выделение внутри этого блока. Проблема с этим подходом заключается в том, что фрагментация памяти становится огромной проблемой для программистов и заставляет их добавлять много логики обработки памяти в свои приложения. В конце концов, сборщик мусора не добавляет много накладных расходов. Выделение выполняется как быстро, так и быстрее, а сжатие обрабатывается автоматически, что позволяет программистам сосредоточиться на своих приложениях.

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

Некоторые люди могут задаться вопросом, почему сборка мусора недоступна в других средах, таких как C или C++. Ответ — типы. Эти языки позволяют приведение указателей к любому типу, что делает чрезвычайно трудно понять, к чему относится указатель. В управляемой среде, такой как среда CLR, мы можем гарантировать достаточное количество указателей, чтобы сделать сборку мусора возможной. Управляемый мир также является единственным местом, где можно безопасно остановить выполнение потока для выполнения сборки мусора: в C++ эти операции являются либо небезопасными, либо очень ограниченными.

Настройка скорости

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

Самая важная эвристика производительности также является самой простой для изучения для программистов, которые привыкли писать машинный код: отслеживайте выделения, которые необходимо сделать, и освобождайте их, когда вы закончите. Сборка мусора не может определить, что вы не собираетесь использовать строку размером 20 КБ, которую вы создали, если она является частью объекта, который хранится вокруг. Предположим, что у вас есть объект, спрятанный в векторе, и вы никогда не собираетесь использовать эту строку снова. Установка для поля значения NULL позволит сборке мусора собирать эти 20 КБ позже, даже если объект по-прежнему нужен для других целей. Если объект больше не нужен, убедитесь, что ссылки на него не сохраняются. (Как и в машинном коде.) Для небольших объектов это меньше проблем. Любой программист, знакомый с управлением памятью в машинном коде, не будет иметь никаких проблем: применяются все те же правила здравого смысла. Вы просто не должны быть таким параноидом о них.

Вторая важная проблема производительности касается очистки объектов. Как я уже упоминал ранее, завершение оказывает глубокое влияние на производительность. Наиболее распространенным примером является управляемый обработчик для неуправляемого ресурса: необходимо реализовать какой-то метод очистки, и именно здесь производительность становится проблемой. Если вы зависите от завершения, вы открываете для себя проблемы с производительностью, перечисленные ранее. Еще следует помнить, что сборка мусора в значительной степени не знает о нехватке памяти в собственном мире, поэтому вы можете использовать тонну неуправляемых ресурсов, просто сохраняя указатель в управляемой куче. Один указатель не занимает много памяти, поэтому может пройти некоторое время, прежде чем потребуется коллекция. Чтобы обойти эти проблемы с производительностью, сохраняя при этом безопасное воспроизведение при хранении памяти, следует выбрать конструктивный шаблон для всех объектов, требующих специальной очистки.

Программист имеет четыре варианта очистки объектов:

  1. Реализация обоих типов

    Это рекомендуемая схема для очистки объектов. Это объект с некоторым сочетанием неуправляемых и управляемых ресурсов. Примером может быть System.Windows.Forms.Control. Он содержит неуправляемый ресурс (HWND) и потенциально управляемые ресурсы (DataConnection и т. д.). Если вы не знаете, когда вы используете неуправляемые ресурсы, вы можете открыть манифест для своей программы в ILDASM`` и проверка для ссылок на собственные библиотеки. Другой альтернативой является использование vadump.exe , чтобы увидеть, какие ресурсы загружаются вместе с программой. И то, и другое может дать вам представление о том, какие собственные ресурсы вы используете.

    Приведенный ниже шаблон предоставляет пользователям один рекомендуемый способ вместо переопределения логики очистки (переопределение Dispose(bool)). Это обеспечивает максимальную гибкость, а также обеспечивает перехват всех на случай, если Dispose() никогда не вызывается. Сочетание максимальной скорости и гибкости, а также подход к обеспечению безопасности делают этот вариант оптимальным для использования.

    Пример

    public class MyClass : IDisposable {
      public void Dispose() {
        Dispose(true);
        GC.SuppressFinalizer(this);
      }
      protected virtual void Dispose(bool disposing) {
        if (disposing) {
          ...
        }
          ...
      }
      ~MyClass() {
        Dispose(false);
      }
    }
    
  2. Реализация только Dispose()

    Это происходит, когда объект имеет только управляемые ресурсы и вы хотите убедиться, что его очистка является детерминированной. Примером такого объекта является System.Web.UI.Control.

    Пример

    public class MyClass : IDisposable {
      public virtual void Dispose() {
        ...
      }
    
  3. Реализация только Finalize()

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

    Пример

    public class MyClass {
      ...
      ~MyClass() {
        ...
      }
    
  4. Реализация ни того, ни

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

Рекомендация

Рекомендации по управлению памятью должны быть знакомы: освобождать объекты после завершения работы с ними и следить за оставлением указателей на объекты. При очистке объектов реализуйте метод Finalize() и Dispose() для объектов с неуправляемыми ресурсами. Это позволит предотвратить непредвиденное поведение в дальнейшем и применить рекомендации по программированию.

Недостатком здесь является то, что вы заставляете людей вызывать Dispose(). Здесь нет потери производительности, но некоторые люди могут оказаться расстраивающими, когда придется думать об удалении своих объектов. Тем не менее, я думаю, что стоит усугубить, чтобы использовать модель, которая имеет смысл. Кроме того, это заставляет людей быть более внимательными к объектам, которые они выделяют, так как они не могут слепо доверять GC всегда заботиться о них. Для программистов из фона C или C++ принудительное вызов Dispose(), вероятно, будет полезным, так как это то, с чем они более знакомы.

Dispose() должен поддерживаться для объектов, которые хранят неуправляемые ресурсы в любом месте дерева объектов под ним; Однако finalize() необходимо размещать только на тех объектах, которые специально удерживаются на этих ресурсах, таких как дескриптор ОС или выделение неуправляемой памяти. Я предлагаю создать небольшие управляемые объекты в качестве "оболочек" для реализации Finalize() в дополнение к поддержке Dispose(),, которая будет вызываться Dispose() родительского объекта. Так как родительские объекты не имеют метода завершения, все дерево объектов не выдержит коллекцию независимо от того, был ли вызван метод Dispose().

Хорошее правило для методов завершения заключается в том, чтобы использовать их только в самом примитивном объекте, требующем завершения. Предположим, что у меня есть большой управляемый ресурс, включающий подключение к базе данных. Я бы позволил завершить само подключение, но сделать остальную часть объекта удаленной. Таким образом, я могу вызвать Dispose() и немедленно освободить управляемые части объекта, не дожидаясь завершения подключения. Помните: используйте Finalize() только там, где это необходимо, если это необходимо.

Примечание Программисты C и C++: семантика деструктора в C# создает метод завершения, а не метод удаления!

Пул потоков

Основы

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

То, что вы знаете из пула потоков NT:

  • Пул потоков будет обрабатывать создание и очистку потока.
  • Он предоставляет порт завершения для потоков ввода-вывода (только для платформ NT).
  • Обратный вызов может быть привязан к файлам или другим системным ресурсам.
  • Доступны API таймера и ожидания.
  • Пул потоков определяет, сколько потоков должно быть активным, используя эвристические методы, такие как задержка с момента последнего внедрения, количество текущих потоков и размер очереди.
  • Потоки из общей очереди.

Что отличается в .NET:

  • Он знает о блокировке потоков в управляемом коде (например, из-за сборки мусора, управляемого ожидания) и может соответствующим образом настроить логику внедрения потоков.
  • Нет гарантии обслуживания для отдельных потоков.

Когда обрабатывать потоки самостоятельно

Эффективное использование пула потоков тесно связано с определением того, что вам нужно от потоков. Если вам нужна гарантия обслуживания, вам потребуется управлять им самостоятельно. В большинстве случаев использование пула обеспечит оптимальную производительность. Если у вас есть жесткие ограничения и требуется жесткий контроль над потоками, скорее всего, имеет смысл использовать собственные потоки в любом случае, поэтому будьте осторожны при обработке управляемых потоков самостоятельно. Если вы решили написать управляемый код и самостоятельно обрабатывать потоки, убедитесь, что вы не создаете потоки для каждого подключения: это только повредит производительности. Как правило, следует выбирать обработку потоков самостоятельно в управляемом мире только в очень конкретных сценариях, где существует большая и трудоемкая задача, которая выполняется редко. Одним из примеров может быть заполнение большого кэша в фоновом режиме или запись большого файла на диск.

Настройка скорости

Пул потоков устанавливает ограничение на количество активных потоков, и если многие из них блокируются, пул будет голодать. В идеале пул потоков следует использовать для кратковременных и неблокирующих потоков. В серверных приложениях необходимо быстро и эффективно отвечать на каждый запрос. Если вы запускаете новый поток для каждого запроса, вы будете иметь дело с большими затратами. Решение заключается в перезапуске потоков, заботясь о том, чтобы очистить и вернуть состояние каждого потока после завершения. Это сценарии, в которых пул потоков является основным преимуществом производительности и проектирования, и где следует использовать эту технологию. Пул потоков обрабатывает очистку состояния и гарантирует, что в данный момент времени используется оптимальное количество потоков. В других ситуациях может быть удобнее обрабатывать потоки самостоятельно.

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

Ниже приведен пример простого приложения, которое использует преимущества пула потоков. Он создает кучу рабочих потоков, а затем выполняет простую задачу перед их закрытием. Я выполнил некоторые проверки ошибок, но это тот же код, который можно найти в папке SDK платформы в разделе "Samples\Threading\Threadpool". В этом примере у нас есть код, который создает простой рабочий элемент и использует пул потоков, чтобы несколько потоков обрабатывали эти элементы без необходимости управлять ими программисту. Дополнительные сведения см. в файле ReadMe.html.

using System;
using System.Threading;

public class SomeState{
  public int Cookie;
  public SomeState(int iCookie){
    Cookie = iCookie;
  }
};


public class Alpha{
  public int [] HashCount;
  public ManualResetEvent eventX;
  public static int iCount = 0;
  public static int iMaxCount = 0;
  public Alpha(int MaxCount) {
    HashCount = new int[30];
    iMaxCount = MaxCount;
  }


   //   The method that will be called when the Work Item is serviced
   //   on the Thread Pool
   public void Beta(Object state){
     Console.WriteLine(" {0} {1} :", 
               Thread.CurrentThread.GetHashCode(), ((SomeState)state).Cookie);
     Interlocked.Increment(ref HashCount[Thread.CurrentThread.GetHashCode()]);

     //   Do some busy work
     int iX = 10000;
     while (iX > 0){ iX--;}
     if (Interlocked.Increment(ref iCount) == iMaxCount) {
       Console.WriteLine("Setting EventX ");
       eventX.Set();
     }
  }
};

public class SimplePool{
  public static int Main(String[] args)   {
    Console.WriteLine("Thread Simple Thread Pool Sample");
    int MaxCount = 1000;
    ManualResetEvent eventX = new ManualResetEvent(false);
    Console.WriteLine("Queuing {0} items to Thread Pool", MaxCount);
    Alpha oAlpha = new Alpha(MaxCount);
    oAlpha.eventX = eventX;
    Console.WriteLine("Queue to Thread Pool 0");
    ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),new SomeState(0));
       for (int iItem=1;iItem < MaxCount;iItem++){
         Console.WriteLine("Queue to Thread Pool {0}", iItem);
         ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),
                                   new SomeState(iItem));
       }
    Console.WriteLine("Waiting for Thread Pool to drain");
    eventX.WaitOne(Timeout.Infinite,true);
    Console.WriteLine("Thread Pool has been drained (Event fired)");
    Console.WriteLine("Load across threads");
    for(int iIndex=0;iIndex<oAlpha.HashCount.Length;iIndex++)
      Console.WriteLine("{0} {1}", iIndex, oAlpha.HashCount[iIndex]);
    }
    return 0;
  }
}

The JIT

Основы

Как и в случае с любой виртуальной машиной, среде CLR требуется способ компиляции промежуточного языка в машинный код. При компиляции программы для запуска в среде CLR компилятор принимает исходный код с высокоуровневого языка до сочетания MSIL (microsoft Intermediate Language) и метаданных. Они объединяются в PE-файл, который затем можно выполнить на любом компьютере с поддержкой СРЕДЫ CLR. При запуске этого исполняемого файла JIT начинает компиляцию IL в машинный код и выполнение этого кода на реальном компьютере. Это делается для каждого метода, поэтому задержка для JITing составляет только столько времени, сколько требуется для кода, который вы хотите запустить.

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

  • Свертывание констант — вычисляет значения констант во время компиляции.

    До После
    x = 5 + 7 x = 12
  • Constant- и Copy-Propagation — замена назад на свободные переменные ранее.

    До После
    x = a x = a
    y = x y = a
    z = 3 + y z = 3 + a
  • Встраивание метода — замените аргументы значениями, переданными во время вызова, и исключите вызов. Затем можно выполнить многие другие оптимизации, чтобы исключить неработает код. Из соображений скорости текущий JIT-код имеет несколько границ на то, что он может встраить. Например, встраиваются только небольшие методы (размер IL меньше 32), а анализ управления потоком довольно примитивен.

    До После
    ...

    x=foo(4, true);

    ...

    }

    foo(int a, bool b){

    if(b){

    return a + 5;

    } else {

    return 2a + bar();

    }

    ...

    x = 9

    ...

    }

    foo(int a, bool b){

    if(b){

    return a + 5;

    } else {

    return 2a + bar();

    }

  • Подъем кода и доминаторы — удалите код из внутренних циклов, если он дублируется снаружи. Приведенный ниже пример "до" фактически создается на уровне IL, так как все индексы массива должны быть проверены.

    До После
    for(i=0; i< a.length;i++){

    if(i < a.length()){

    a[i] = null

    } else {

    raise IndexOutOfBounds;

    }

    }

    for(int i=0; i<a.length; i++){

    a[i] = null;

    }

  • Unrolling Loop Unrolling — затраты на увеличение счетчиков и выполнение теста можно удалить, а код цикла можно повторить. Для очень жестких циклов это приводит к победе в производительности.

    До После
    for(i=0; i< 3; i++){

    print("flaming monkeys!");

    }

    print("flaming monkeys!");

    print("flaming monkeys!");

    print("flaming monkeys!");

  • Common SubExpression Elimination — если динамическая переменная по-прежнему содержит пересчитываемые сведения, используйте ее вместо этого.

    До После
    x = 4 + y

    z = 4 + y

    x = 4 + y

    z = x

  • Enregistration — здесь нет смысла давать пример кода, поэтому достаточно пояснений. Эта оптимизация может потратить время на то, как локальные и временные значения используются в функции, и попытаться обрабатывать назначение регистров как можно эффективнее. Это может быть чрезвычайно дорогостоящей оптимизацией, и текущий JIT-код СРЕДЫ CLR учитывает не более 64 локальных переменных для регистрации. Переменные, которые не учитываются, помещаются в кадр стека. Это классический пример ограничений JITing: хотя это нормально в 99 % случаев, очень необычные функции с более чем 100 локальными значениями будут лучше оптимизированы с помощью традиционной, отнимающей много времени предварительной компиляции.

  • Прочее— выполняются другие простые оптимизации, но приведенный выше список является хорошим примером. JIT также проходит для недоставленного кода и других оптимизаций глазков.

Когда код получает JITed?

Ниже приведен путь, через который проходит код при его выполнении:

  1. Программа загружается, а таблица функций инициализируется указателями, ссылающимися на IL.
  2. Метод Main является JITed в машинный код, который затем выполняется. Вызовы функций компилируются в непрямые вызовы функций через таблицу.
  3. При вызове другого метода время выполнения просматривает таблицу, чтобы узнать, указывает ли она на код JITed.
    1. Если он имеет (возможно, он был вызван с другого сайта вызова или был предварительно скомпилирован), поток управления продолжается.
    2. В противном случае используется метод JITed, а таблица обновляется.
  4. По мере их вызова все больше и больше методов компилируются в машинный код, а все больше записей в таблице указывают на растущий пул инструкций x86.
  5. По мере запуска программы JIT вызывается все реже и реже, пока все не будет скомпилировано.
  6. Метод не будет JITed, пока не будет вызван, а затем он никогда не будет JITed снова во время выполнения программы. Вы платите только за то, что используете.

Миф: jited программы выполняются медленнее, чем предкомпилированные программы

Так бывает редко. Издержки, связанные с JITing несколькими методами, незначительны по сравнению с временем, затраченным на чтение нескольких страниц с диска, и методы имеют значение JITed только по мере необходимости. Время, затраченное на JIT, настолько незначительно, что оно почти не заметно, и после того, как метод был JITed, вы никогда не понесете затраты на этот метод снова. Подробнее об этом мы поговорим в разделе Предварительная компиляция кода.

Как упоминалось выше, JIT версии 1 (версия 1) выполняет большую часть оптимизаций, чем компилятор, и будет быстрее в следующей версии (vNext) по мере добавления более сложных оптимизаций. Что еще более важно, JIT может выполнять некоторые оптимизации, которые не может выполнять обычный компилятор, например оптимизацию ЦП и настройку кэша.

Оптимизация JIT-Only

Так как JIT-код активируется во время выполнения, компилятору не известно много сведений. Это позволяет выполнить несколько оптимизаций, доступных только во время выполнения:

  • Оптимизация для конкретного процессора. Во время выполнения JIT-код знает, может ли он использовать инструкции SSE или 3DNow. Исполняемый файл будет скомпилирован специально для P4, Athlon или любых будущих семейств процессоров. Вы развертываете один раз, и тот же код улучшится вместе с JIT и компьютером пользователя.
  • Оптимизация уровней косвенного обращения, так как функция и расположение объекта доступны во время выполнения.
  • JIT-код может выполнять оптимизацию между сборками, обеспечивая множество преимуществ, которые вы получаете при компиляции программы со статическими библиотеками, но сохраняя гибкость и небольшой объем использования динамических библиотек.
  • Агрессивные встроенные функции , которые вызываются чаще, так как ей известно о потоке управления во время выполнения. Оптимизация может значительно повысить скорость, и есть много возможностей для дополнительного улучшения vNext.

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

Предварительная компиляция кода (с помощью ngen.exe)

Для поставщика приложений возможность предварительной компиляции кода во время установки является привлекательным вариантом. Корпорация Майкрософт предоставляет этот параметр в формате ngen.exe, который позволит запустить обычный JIT-компилятор для всей программы один раз и сохранить результат. Так как оптимизация только во время выполнения не может быть выполнена во время предварительной компиляции, создаваемый код обычно не так хорош, как при обычном JIT-коде. Однако без использования JIT-методов на лету стоимость запуска значительно ниже, а некоторые программы запускатся заметно быстрее. В будущем ngen.exe может выполнять больше, чем просто выполнять JIT-код во время выполнения: более агрессивные оптимизации с более высокими границами, чем во время выполнения, взаимодействие разработчиков с оптимизацией порядка нагрузки (оптимизация способа упакованного кода в страницы виртуальной машины) и более сложные, трудоемкие оптимизации, которые могут воспользоваться преимуществами времени во время предварительной компиляции.

Сокращение времени запуска помогает в двух случаях, и для всего остального оно не конкурирует с оптимизацией только во время выполнения, которую может выполнять обычный JITing. Первая ситуация заключается в том, что вы вызываете огромное количество методов на ранних этапах программы. Вам придется заранее выполнить JIT-код, что приведет к неприемлемой загрузке. Это не будет так для большинства людей, но до JITing может иметь смысл, если это влияет на вас. Предварительная компиляция также имеет смысл в случае с большими общими библиотеками, так как вы платите за их загрузку гораздо чаще. Корпорация Майкрософт предварительно компилирует платформы для среды CLR, так как большинство приложений будут использовать их.

Это легко использовать ngen.exe , чтобы увидеть, если предварительная компиляция является ответом для вас, поэтому я рекомендую попробовать его. Однако в большинстве случаев лучше использовать обычный JIT-файл и воспользоваться преимуществами оптимизации во время выполнения. Они имеют огромную отдачу, и в большинстве случаев они будут более чем компенсировать одноразовые затраты на запуск.

Настройка скорости

Для программиста, есть только две вещи, которые стоит отметить. Во-первых, что JIT очень умен. Не пытайтесь выдумать компилятор. Код, как обычно. Предположим, что у вас есть следующий код:

...

for(int i = 0; i < myArray.length; i++){

...

}

...

...

int l = myArray.length;

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

...

}

...

Некоторые программисты считают, что они могут получить повышение скорости, переместив вычисление длины и сохранив его до темпа, как в примере справа.

Правда в том, что подобные оптимизации не были полезны в течение почти 10 лет: современные компиляторы более чем способны выполнить эту оптимизацию за вас. На самом деле, иногда такие вещи могут на самом деле повредить производительности. В приведенном выше примере компилятор, вероятно, проверка, чтобы увидеть, что длина myArray является константой, и вставить константу в сравнение цикла for. Но код справа может заставить компилятор задуматься о том, что это значение должно храниться в регистре, так как l оно находится в активном цикле. В итоге напишите наиболее удобочитаемый и наиболее удобочитаемый код. Это не поможет, чтобы попытаться издумать компилятор, и иногда это может повредить.

Вторая вещь, о чем нужно говорить, это tail-calls. На данный момент компиляторы C# и Microsoft® Visual Basic® не предоставляют возможность указать, что следует использовать вызов заключительного фрагмента. Если вам действительно нужна эта функция, один из вариантов — открыть PE-файл в дизассемблировщике и использовать вместо него инструкцию MSIL .tail. Это не элегантное решение, но в C# и Visual Basic не так полезны, как в таких языках, как Scheme или ML. Люди написания компиляторов для языков, которые действительно используют преимущества конечных вызовов, должны обязательно использовать эту инструкцию. Реальность для большинства людей заключается в том, что даже вручную настройка IL для использования tail-звонки не обеспечивает огромное преимущество скорости. Иногда время выполнения фактически меняет их на обычные вызовы по соображениям безопасности! Возможно, в будущих версиях будет приложено больше усилий для поддержки хвостовых вызовов, но на данный момент повышения производительности недостаточно, чтобы оправдать это, и очень немногие программисты захотят воспользоваться этим.

Домены приложений

Основы

Межпроцессная связь становится все более распространенной. Из соображений стабильности и безопасности ОС хранит приложения в отдельных адресных пространствах. Простым примером является способ выполнения всех 16-разрядных приложений в NT: при выполнении в отдельном процессе одно приложение не может помешать выполнению другого. Проблема здесь заключается в стоимости переключения контекста и открытия соединения между процессами. Эта операция очень дорогая, и очень вредит производительности. В серверных приложениях, в которых часто размещается несколько веб-приложений, это серьезное снижение производительности и масштабируемости.

В среде CLR представлена концепция AppDomain, которая похожа на процесс в том, что это автономное пространство для приложения. Однако домены приложений не ограничиваются одним процессом. В одном процессе можно запустить два совершенно несвязанных доменов приложений благодаря безопасности типов, обеспечиваемой управляемым кодом. Повышение производительности здесь является огромным для ситуаций, когда вы обычно тратите много времени выполнения на межпроцессный обмен данными: IPC между сборками в пять раз быстрее, чем между процессами в NT. Значительно снижая эти затраты, вы получаете как повышение скорости, так и новый вариант во время разработки программы: теперь имеет смысл использовать отдельные процессы, где раньше это могло быть слишком дорогим. Возможность запуска нескольких программ в одном процессе с той же безопасностью, что и раньше, имеет огромные последствия для масштабируемости и безопасности.

Поддержка доменов приложений отсутствует в ОС. Домены приложений обрабатываются узлом CLR, например в ASP.NET, исполняемом файле оболочки или Microsoft Internet Обозреватель. Вы также можете написать свои собственные. Каждый узел указывает домен по умолчанию, который загружается при первом запуске приложения и закрывается только после завершения процесса. При загрузке других сборок в процесс можно указать, что они будут загружены в определенный домен приложения, и задать разные политики безопасности для каждой из них. Более подробно это описано в документации microsoft платформа .NET Framework SDK.

Настройка скорости

Чтобы эффективно использовать AppDomains, необходимо подумать о том, какой тип приложения вы пишете и какую работу ему нужно выполнить. Как правило, домены приложений наиболее эффективны, если приложение соответствует некоторым из следующих характеристик:

  • Он часто порождает новую копию себя.
  • Он работает с другими приложениями для обработки информации (например, запросов к базе данных на веб-сервере).
  • Он проводит много времени в IPC с программами, которые работают исключительно с вашим приложением.
  • Он открывает и закрывает другие программы.

Пример ситуации, в которой домены приложений полезны, можно увидеть в сложном ASP.NET приложении. Предположим, вы хотите обеспечить изоляцию между разными виртуальными ролями: в собственном пространстве необходимо поместить каждую виртуальную сеть в отдельный процесс. Это довольно дорого, и переключение контекста между ними сопряжено с большими затратами. В управляемом мире каждая виртуальная сеть может быть отдельным доменом приложения. Это позволяет сохранить необходимую изоляцию, резко сокращая затраты.

Домены приложений следует использовать только в том случае, если приложение достаточно сложное, чтобы требовать тесной работы с другими процессами или другими экземплярами. Хотя обмен данными iter-AppDomain выполняется гораздо быстрее, чем обмен данными между процессами, стоимость запуска и закрытия appDomain на самом деле может быть дороже. Домены приложений могут ухудшить производительность при использовании по неправильным причинам, поэтому убедитесь, что вы используете их в правильных ситуациях. Обратите внимание, что в AppDomain можно загрузить только управляемый код, так как безопасность неуправляемого кода не гарантируется.

Чтобы сохранить изоляцию между доменами, сборки, совместно используемые несколькими доменами AppDomain, должны иметь jiTed для каждого домена. Это приводит к созданию большого количества дубликатов кода и трате памяти. Рассмотрим случай приложения, которое отвечает на запросы с помощью какой-либо службы XML. Если некоторые запросы должны быть изолированы друг от друга, их необходимо перенаправить в разные домены приложений. Проблема заключается в том, что для каждого домена Приложения теперь требуются одни и те же библиотеки XML, а та же сборка будет загружаться несколько раз.

Один из способов обойти это — объявить сборку как нейтрализуемую доменом, а это означает, что прямые ссылки не допускаются, а изоляция обеспечивается путем косвенного обращения. Это экономит время, так как сборка имеет значение JITed только один раз. Это также экономит память, так как ничего не дублируется. К сожалению, есть снижение производительности из-за необходимости косвенного обращения. Объявление сборки нейтрализующейся к домену приводит к повышению производительности, если проблема связана с памятью или когда код JITing тратит слишком много времени. Подобные сценарии являются распространенными в случае большой сборки, совместно используемой несколькими доменами.

Безопасность

Основы

Безопасность доступа к коду — это мощная и чрезвычайно полезная функция. Он обеспечивает пользователям безопасное выполнение полунадежного кода, защищает от вредоносных программ и нескольких видов атак, а также обеспечивает управляемый доступ к ресурсам на основе удостоверений. В машинном коде обеспечить безопасность крайне сложно, так как существует мало типовой безопасности, и программист обрабатывает память. В среде CLR время выполнения достаточно знает о выполнении кода, чтобы добавить надежную поддержку безопасности, которая является новой для большинства программистов функцией.

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

Настройка скорости

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

  • Декларативная безопасность — это самый быстрый способ использования Assert, Deny и PermitOnly. Для этих операций обычно требуется пошаговое руководство по стеку, чтобы найти правильный кадр вызова, но этого можно избежать, если явно объявить эти модификаторы. Требования выполняются быстрее, если они выполняются императивно.
  • При взаимодействии с неуправляемыми кодами можно удалить проверки безопасности во время выполнения с помощью атрибута SuppressUnmanagedCodeSecurity. Это перемещает проверка во время связи, что происходит гораздо быстрее. С осторожностью убедитесь, что код не содержит дыр в безопасности для другого кода, который может использовать удаленный проверка в небезопасный код.
  • Проверки удостоверений стоят дороже, чем проверки кода. Вместо этого можно использовать LinkDemand для выполнения этих проверок во время ссылки.

Существует два способа оптимизации безопасности:

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

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

Удаленное взаимодействие

Основы

Технология удаленного взаимодействия в .NET расширяет многофункциональную систему типов и функциональные возможности среды CLR по сети. С помощью XML, SOAP и HTTP можно вызывать процедуры и передавать объекты удаленно, как если бы они размещались на одном компьютере. Это можно представить как версию .NET DCOM или CORBA, так как она предоставляет надмножество их функциональных возможностей.

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

Настройка скорости

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

  • Делайте ненавязкие звонки вместо чата. Узнайте, можно ли сократить количество звонков, которые необходимо совершать удаленно. Например, предположим, что вы задали некоторые свойства для удаленного объекта с помощью методов get() и set(). Это сэкономит время на удаленное повторное создание объекта с этими свойствами, заданными при создании. Так как это можно сделать с помощью одного удаленного вызова, вы сэкономите время, потраченное на сетевой трафик. Иногда имеет смысл переместить объект на локальный компьютер, задать там свойства, а затем скопировать его обратно. В зависимости от пропускной способности и задержки иногда одно решение имеет больше смысла, чем другое.
  • Балансировка нагрузки на ЦП с сетевой нагрузкой. Иногда имеет смысл отправить что-то для выполнения по сети, а в других случаях лучше выполнять работу самостоятельно. Если вы тратите много времени на обход сети, производительность будет снижена. Если вы используете слишком много ресурсов ЦП, вы не сможете отвечать на другие запросы. Найти хороший баланс между этими двумя важно для масштабирования приложения.
  • Используйте асинхронные вызовы. При выполнении вызова по сети убедитесь, что он является асинхронным, если не требуется иное. В противном случае приложение будет застопориться, пока не получит ответ, что может быть неприемлемо в пользовательском интерфейсе или на сервере большого объема. Хороший пример доступен в пакете SDK для платформы, который поставляется с .NET, в разделе "Samples\technologies\remoting\advanced\asyncdelegate".
  • Использовать объекты Оптимально— можно указать, что новый объект создается для каждого запроса (SingleCall) или что один и тот же объект используется для всех запросов (singleton). Наличие единого объекта для всех запросов, безусловно, менее ресурсоемко, но необходимо быть осторожным с синхронизацией и конфигурацией объекта от запроса к запросу.
  • Использование подключаемых каналов и модулей форматирования. Мощная функция удаленного взаимодействия — возможность подключения любого канала или модуля форматирования к приложению. Например, если вам не нужно пройти через брандмауэр, нет причин использовать http-канал. Подключение к каналу TCP обеспечит гораздо более высокую производительность. Убедитесь, что выбран канал или модуль форматирования, который вам подходит.

ValueTypes

Основы

Гибкость, которую обеспечивают объекты, достигается за небольшую цену производительности. Для выделения, доступа и обновления объектов, управляемых кучи, требуется больше времени, чем для объектов, управляемых стеком. Вот почему, например, структуру в C++ гораздо эффективнее, чем объект . Конечно, объекты могут выполнять действия, которые не могут выполнять структуры, и являются гораздо более универсальными.

Но иногда вам не нужна вся эта гибкость. Иногда требуется что-то такое простое, как структуру, и вы не хотите платить за производительность. Среда CLR позволяет указать, что называется ValueType, и во время компиляции это обрабатывается так же, как структуру. ValueTypes управляются стеком и обеспечивают всю скорость структуры. Как и ожидалось, они также поставляются с ограниченной гибкостью структур (например, нет наследования). Но для тех экземпляров, где все, что вам нужно, это структуру, ValueTypes обеспечивают невероятное повышение скорости. Более подробные сведения о ValueTypes и остальной системе типов CLR доступны на библиотека MSDN.

Настройка скорости

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

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

using System;
using System.Collections;

namespace ConsoleApplication{
  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
class Class1{
  static void Main(string[] args){
    Console.WriteLine("starting struct loop....");
    int t1 = Environment.TickCount;
    for (int i = 0; i < 25000000; i++) {
      foo test1 = new foo(3.14);
      foo test2 = new foo(3.15);
       if (test1.y == test2.y) break; // prevent code from being 
       eliminated JIT
    }
    int t2 = Environment.TickCount;
    Console.WriteLine("struct loop: (" + (t2-t1) + "). starting object 
       loop....");
    t1 = Environment.TickCount;
    for (int i = 0; i < 25000000; i++) {
      bar test1 = new bar(3.14);
      bar test2 = new bar(3.15);
      if (test1.y == test2.y) break; // prevent code from being 
      eliminated JIT
    }
    t2 = Environment.TickCount;
    Console.WriteLine("object loop: (" + (t2-t1) + ")");
    }

Попробуйте самостоятельно. Интервал времени составляет порядка нескольких секунд. Теперь давайте изменим программу, чтобы время выполнения было упаковать и распаковать нашу структуру. Обратите внимание, что преимущества скорости использования ValueType полностью исчезли. Мораль здесь заключается в том, что ValueTypes используются только в крайне редких ситуациях, когда вы не используете их в качестве объектов. Важно обратить внимание на эти ситуации, так как производительность часто очень велика, когда вы используете их правильно.

using System;
using System.Collections;

namespace ConsoleApplication{
  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      Hashtable boxed_table = new Hashtable(2);
      Hashtable object_table = new Hashtable(2);
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 10000000; i++){
        boxed_table.Add(1, new foo(3.14)); 
        boxed_table.Add(2, new foo(3.15));
        boxed_table.Remove(1);
      }
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 10000000; i++){
        object_table.Add(1, new bar(3.14)); 
        object_table.Add(2, new bar(3.15));
        object_table.Remove(1);
      }
      System.Console.WriteLine("All done");
    }
  }
}

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

Следует отметить, что для типов ValueType не требуется маршалирование в сценариях взаимодействия. Так как маршалинг является одним из самых больших хитов производительности при взаимодействии с машинным кодом, использование ValueTypes в качестве аргументов для собственных функций, возможно, является самой большой настройкой производительности, которое вы можете сделать.

Дополнительные ресурсы

Ниже приведены следующие разделы, посвященные производительности в платформа .NET Framework.

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

Приложение. Размещение времени выполнения сервера

#include "mscoree.h"
#include "stdio.h"
#import "mscorlib.tlb" named_guids no_namespace raw_interfaces_only \
no_implementation exclude("IID_IObjectHandle", "IObjectHandle")

long main(){
  long retval = 0;
  LPWSTR pszFlavor = L"svr";

  // Bind to the Run time.
  ICorRuntimeHost *pHost = NULL;
  HRESULT hr = CorBindToRuntimeEx(NULL,
               pszFlavor, 
               NULL,
               CLSID_CorRuntimeHost, 
               IID_ICorRuntimeHost, 
               (void **)&pHost);

  if (SUCCEEDED(hr)){
    printf("Got ICorRuntimeHost\n");
      
    // Start the Run time (this also creates a default AppDomain)
    hr = pHost->Start();
    if(SUCCEEDED(hr)){
      printf("Started\n");
         
      // Get the Default AppDomain created when we called Start
      IUnknown *pUnk = NULL;
      hr = pHost->GetDefaultDomain(&pUnk);

      if(SUCCEEDED(hr)){
        printf("Got IUnknown\n");
            
        // Ask for the _AppDomain Interface
        _AppDomain *pDomain = NULL;
        hr = pUnk->QueryInterface(IID__AppDomain, (void**)&pDomain);
            
        if(SUCCEEDED(hr)){
          printf("Got _AppDomain\n");
               
          // Execute Assembly's entry point on this thread
          BSTR pszAssemblyName = SysAllocString(L"Managed.exe");
          hr = pDomain->ExecuteAssembly_2(pszAssemblyName, &retval);
          SysFreeString(pszAssemblyName);
               
          if (SUCCEEDED(hr)){
            printf("Execution completed\n");

            //Execution completed Successfully
            pDomain->Release();
            pUnk->Release();
            pHost->Stop();
            
            return retval;
          }
        }
        pDomain->Release();
        pUnk->Release();
      }
    }
    pHost->Release();
  }
  printf("Failure, HRESULT: %x\n", hr);
   
  // If we got here, there was an error, return the HRESULT
  return hr;
}

Если у вас есть вопросы или комментарии по поводу этой статьи, обратитесь к Клаудио Колдато, руководитель программы по вопросам платформа .NET Framework проблем с производительностью.