Повышение производительности управляемого кода

 

Ян Грей
Команда по повышению производительности Microsoft CLR

Июнь 2003 г.

Область применения:
   Microsoft® платформа .NET Framework

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

Скачайте профилировщик CLR. (330 КБ)

Содержимое

Введение (и обязательство)
На пути к модели затрат для управляемого кода
Стоимость управляемого кода
Заключение
Ресурсы

Введение (и обязательство)

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

Не совершайте медленный и жирный код на мире. Вы не презираете такой код? Код, который выполняется в соответствии и запускается? Код, который блокирует пользовательский интерфейс на секунды? Код, который привязывает ЦП к диску?

Не делайте этого. Вместо этого, встаньте и пообещайте вместе со мной:

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

(Действительно.) Так ты обещала? Молодец.

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

Но вы не можете сделать разумный выбор среди альтернативных вариантов, если не знаете, что стоит: вы не можете написать эффективный код, если не знаете, сколько стоит.

В старые добрые времена было легче. Хорошие программисты C знали. Каждый оператор и операция в C, будь то присваивание, целое число или математические вычисления с плавающей запятой, разыменовка или вызов функции, сопоставляются более или менее "один к одному" с одной примитивной операцией компьютера. Правда, иногда требовалось несколько машинных инструкций, чтобы поместить правильные операнды в правильные регистры, а иногда одна инструкция могла записать несколько операций C (лихо *dest++ = *src++;), но обычно можно было написать (или прочитать) строку кода C и знать, куда идет время. Как для кода, так и для данных компилятор C был WYWIWYG — "то, что вы пишете, это то, что вы получаете". (Исключением были и являются вызовы функций. Если вы не знаете, сколько стоит функция, вы не знаете, действительно.)

В 1990-х годах, чтобы воспользоваться многими преимуществами проектирования программного обеспечения и производительности абстракции данных, объектно-ориентированного программирования и повторного использования кода, индустрия программного обеспечения ДЛЯ ПК сделала переход с C на C++.

C++ — это надмножество C, и он "оплачивается по мере использования" — новые функции ничего не стоят, если вы их не используете, поэтому опыт программирования на C, включая интернализованную модель затрат, применим напрямую. Если взять рабочий код C и перекомпилировать его для C++, затраты на время выполнения и пространство не должны сильно измениться.

С другой стороны, В C++ представлено множество новых возможностей языка, включая конструкторы, деструкторы, новые, удаление, одиночное, несколько и виртуальных наследование, приведения, функции-члены, виртуальные функции, перегруженные операторы, указатели на члены, массивы объектов, обработку исключений и композиции, которые влечет за собой нетривиальные скрытые затраты. Например, виртуальные функции стоят два дополнительных косвенных обращения за вызов и добавляют скрытое поле указателя vtable к каждому экземпляру. Или рассмотрим, что этот безобидный код:

{ complex a, b, c, d; … a = b + c * d; }

компилируется примерно в тринадцать неявных вызовов функций-членов (надеюсь, встраиваются).

Девять лет назад мы изучили эту тему в моей статье C++: Под капотом. Я написала:

"Важно понимать, как реализуется ваш язык программирования. Такие знания развеивает страх и удивление "Что на земле компилятор делает здесь?"; придает уверенность в использовании новых функций; и предоставляет аналитические сведения при отладке и изучении других языковых функций. Это также дает представление о относительных затратах на различные варианты кодирования, которые необходимы для написания наиболее эффективного кода изо дня в день".

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

И сдержим свои обещания.

Почему управляемый код?

Для подавляющего большинства разработчиков машинного кода управляемый код является более эффективной платформой для запуска программного обеспечения. Он удаляет целые категории ошибок, таких как повреждения кучи и ошибки, связанные с индексом массива, которые так часто приводят к разочаровывание ночных сеансов отладки. Он поддерживает современные требования, такие как безопасный мобильный код (с помощью безопасности доступа к коду) и веб-службы XML, и по сравнению со стареющим Win32/COM/ATL/MFC/VB, платформа .NET Framework — это обновляющий чистый макет сланца, где вы можете сделать больше с меньшими усилиями.

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

В чем секрет написания более быстрого управляемого кода?

Просто потому, что вы можете сделать больше с меньшими усилиями, не является лицензией, чтобы отказаться от ответственности за код мудро. Во-первых, вы должны признаться в этом себе: "Я новичок". Ты новичок. Я тоже новичок. Мы все младенца в управляемом коде земли. Мы все еще изучаем веревки, в том числе то, что стоит.

Когда дело доходит до богатых и удобных платформа .NET Framework, это как мы дети в магазине конфет. "Ничего себе, я не должен делать все, что муторные strncpy вещи, я могу просто "+" строки вместе! Ничего себе, я могу загрузить мегабайт XML в нескольких строках кода! Ву-ху!"

Все так просто. Так просто, действительно. Так легко сжечь мегабайты ОЗУ, анализируя НАБОРы сведений XML, просто чтобы извлечь из них несколько элементов. В C или C++ это было настолько болезненным, что вы бы дважды подумали, что, возможно, вы создадите конечный автомат на основе api, подобного SAX. С помощью платформа .NET Framework вы просто загружаете весь набор сведений в одном gulp. Может быть, вы даже делаете это снова и снова. Тогда, возможно, ваше приложение больше не кажется таким быстрым. Может быть, он имеет рабочий набор из многих мегабайт. Может быть, вам стоило подумать дважды о том, что эти легкие методы стоят ...

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

Так как мы все новички здесь, и поскольку мы не знаем, что стоит, и поскольку затраты не четко документированы, что нам делать?

Измерьте его. Секрет заключается в том, чтобы измерить его и быть бдительным. Нам всем придется приобрести привычку измерять стоимость вещей. Если мы пойдем к проблеме измерения того, что стоят вещи, то мы не будем теми, кто непреднамеренно вызывает whizzy новый метод, который стоит в десять раз больше, чем мы предполагали , что это стоит.

(Кстати, чтобы получить более глубокое представление о производительности, лежащих в основе BCL (библиотеки базовых классов) или самой среды CLR, рассмотрите возможность ознакомиться с интерфейсом командной строки общего источника, т. е. Ротором. Код Ротора имеет общую черту крови с платформа .NET Framework и СРЕДОЙ CLR. Это не тот же код во всем, но даже в этом случае, я обещаю вам, что вдумчивое изучение Ротора даст вам новое представление о происходящем под капотом CLR. Но сначала обязательно ознакомьтесь с лицензией SSCLI!)

Знания

Если вы стремитесь быть таксистом в Лондоне, вы должны сначала заработать Знания. Студенты учатся в течение многих месяцев, чтобы запомнить тысячи маленьких улиц в Лондоне и узнать лучшие маршруты от места к месту. И они выходят каждый день на скутеры, чтобы разведывать вокруг и укрепить их книги обучения.

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

Знания нет ни в одной книге, увы. Вы должны выйти на скутер и изучить, то есть прокрутить csc, ildasm, отладчик VS.NET, CLR Profiler, ваш профилировщик, некоторые таймеры производительности и т. д., и посмотреть, сколько времени и пространства стоит ваш код.

На пути к модели затрат для управляемого кода

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

(Это не позволит решить транзитивные затраты на вызов методов или методов платформа .NET Framework. Это придется подождать еще одну статью в другой день.)

Ранее я уже говорил, что большая часть модели затрат на C по-прежнему применяется в сценариях C++. Аналогичным образом большая часть модели затрат C/C++ по-прежнему применяется к управляемому коду.

Как это может быть? Вы знаете модель выполнения СРЕДЫ CLR. Вы пишете код на одном из нескольких языков. Скомпилируйте его в формате CIL (Common Intermediate Language), упаковав в сборки. Вы запускаете сборку приложения main, и она начинает выполнять CIL. Но разве это не на порядок медленнее, как интерпретаторы байт-кода старых?

JIT-компилятор

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

В отличие от традиционного автономного процесса компиляции C++, время, затраченное в JIT-компиляторе, является задержкой в "стенных часах" для каждого пользователя, поэтому JIT-компилятор не имеет возможности полной оптимизации. Несмотря на это, список оптимизаций, которые выполняет JIT-компилятор, впечатляет:

  • Свертывание констант
  • Распространение констант и копий
  • Исключение общих частей выражений
  • Инвариантное движение цикла в коде
  • Неработает с хранилищем и устранением неработаемого кода
  • Выделение регистра
  • Встраивание метода
  • Развертывание цикла (небольшие циклы с небольшими телами)

Результат сравним с традиционным машинным кодом , по крайней мере в той же ballpark.

Что касается данных, вы будете использовать сочетание типов значений или ссылочных типов. Типы значений, включая целочисленные типы, типы с плавающей запятой, перечисления и структуры, обычно живут в стеке. Они так же малы и быстры, как локальные и структуры в C/C++. Как и в случае с C/C++, скорее всего, следует избегать передачи больших структур в качестве аргументов метода или возвращаемых значений, так как затраты на копирование могут быть слишком дорогостоящими.

Ссылочные типы и упакованные типы значений живут в куче. Они адресуются с помощью ссылок на объекты, которые являются просто указателями компьютера, как и указатели на объекты в C/C++.

Таким образом, управляемый код может выполняться быстро. За некоторыми исключениями, которые мы рассмотрим ниже, если вы чувствуете стоимость некоторого выражения в машинном коде C, вы не будете идти далеко неправильно моделировать его стоимость как эквивалент в управляемом коде.

Я также должен упоминание NGEN, инструмент, который "заранее" компилирует CIL в сборки машинного кода. Хотя nGEN'ing ваши сборки в настоящее время не оказывает существенного влияния (хорошее или плохое) на время выполнения, это может сократить общий рабочий набор для общих сборок, которые загружаются во многие домены приложений и процессы. (Операционная система может совместно использовать одну копию кода NGEN на всех клиентах; в то время как фрагмент кода обычно не используется в доменах приложений или процессах. Но см. также LoaderOptimizationAttribute.MultiDomain.)

Automatic Memory Management

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

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

Но выделение объектов по-прежнему не является бесплатным. Объекты занимают место. Безудержное выделение объектов приводит к более частым циклам сборки мусора.

Что еще хуже, неоправданное сохранение ссылок на бесполезные графы объектов сохраняет их в живых. Иногда мы видим скромные программы с плачевными рабочими наборами 100 МБ, авторы которых отрицают свою вину и вместо этого приписывают их низкую производительность какой-то таинственной, неопознанной (и, следовательно, неразрешимой) проблеме с управляемым кодом. Это трагично. Но затем час изучения с помощью профилировщика CLR и изменений в нескольких строках кода сокращает использование кучи в десять или более раз. Если вы столкнулись с проблемой большого рабочего набора, первым шагом является поиск в зеркало.

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

Это также относится к проектированию API. Можно спроектировать тип и его методы таким образом, чтобы клиенты создавали новые объекты с диким отказом. Не делайте этого.

Стоимость управляемого кода

Теперь рассмотрим временные затраты на различные низкоуровневые операции с управляемым кодом.

В таблице 1 представлена приблизительная стоимость различных низкоуровневых операций с управляемым кодом в наносекундах на низком уровне на компьютере с тактовой частотой 1,1 ГГц под управлением Windows XP и платформа .NET Framework версии 1.1 ("Everett"), собранном с набором простых циклов времени.

Драйвер теста вызывает каждый метод теста, указывая количество итераций для выполнения, автоматически масштабируемых для итерации от 218 до 230 итераций, при необходимости для выполнения каждого теста в течение не менее 50 мс. Как правило, это достаточно долго, чтобы наблюдать несколько циклов сборки мусора поколения 0 в тесте, который выполняет интенсивное выделение объектов. В таблице показаны средние результаты более 10 испытаний, а также лучшие (минимальное время) пробы для каждого субъекта теста.

Каждый цикл тестирования по мере необходимости выполняется от 4 до 64 раз, чтобы уменьшить затраты на тестовый цикл. Я проверил машинный код, созданный для каждого теста, чтобы убедиться, что JIT-компилятор не оптимизировал тест. Например, в нескольких случаях я изменял тест так, чтобы промежуточные результаты сохранялись во время и после цикла тестирования. Аналогичным образом я внес изменения, чтобы исключить исключение общей части выражения в нескольких тестах.

Таблица 1 Примитивное время (среднее и минимальное) (ns)

Avg Min Примитивные Avg Min Примитивные Avg Min Примитивные
0,0 0,0 Control 2.6 2.6 новый valtype L1 0,8 0,8 isinst up 1
1.0 1.0 Int add 4.6 4.6 новый вальтип L2 0,8 0,8 isinst down 0
1.0 1.0 Int sub 6.4 6.4 новый valtype L3 6.3 6.3 isinst down 1
2.7 2.7 Int mul 8.0 8.0 новый valtype L4 10,7 10.6 isinst (up 2) down 1
35,9 35,7 Int div 23,0 22,9 новый valtype L5 6.4 6.4 isinst down 2
2.1 2.1 Int shift 22,0 20,3 новый ссылочный тип L1 6.1 6.1 isinst down 3
2.1 2.1 long add 26,1 23,9 новый ссылочный тип L2 1.0 1.0 Поле get
2.1 2.1 long sub 30,2 27,5 новый ссылочный тип L3 1,2 1,2 get prop
34,2 34,1 long mul 34,1 30.8 новый ссылочный тип L4 1,2 1,2 поле set
50,1 50,0 long div 39.1 34.4 new reftype L5 1,2 1,2 set prop
5,1 5,1 длинная смена 22,3 20,3 new reftype empty ctor L1 0,9 0,9 Получить это поле
1,3 1,3 float add 26,5 23,9 new reftype empty ctor L2 0,9 0,9 получить эту опору
1.4 1.4 float sub 38.1 34.7 new reftype empty ctor L3 1,2 1,2 Задать это поле
2.0 2.0 float mul 34.7 30,7 new reftype empty ctor L4 1,2 1,2 установить эту опору
27,7 27.6 float div 38.5 34.3 new reftype empty ctor L5 6.4 6.3 получить виртуальную опору
1.5 1.5 double add 22,9 20,7 new reftype ctor L1 6.4 6.3 set virtual prop
1.5 1.5 double sub 27.8 25.4 new reftype ctor L2 6.4 6.4 Барьер записи
2.1 2,0 double mul 32,7 29,9 new reftype ctor L3 1,9 1,9 load int array elem
27,7 27.6 double div 37.7 34,1 new reftype ctor L4 1,9 1,9 store int array elem
0,2 0,2 встроенный статический вызов 43.2 39.1 new reftype ctor L5 2,5 2,5 load obj array elem
6.1 6.1 статический вызов 28,6 26,7 new reftype ctor no-inl L1 16,0 16,0 store obj array elem
1,1 1.0 вызов встроенного экземпляра 38.9 36,5 new reftype ctor no-inl L2 29,0 21,6 box int
6,8 6,8 вызов экземпляра 50.6 47.7 new reftype ctor no-inl L3 3,0 3,0 распаковка int
0,2 0,2 Inlined this inst call 61.8 58.2 new reftype ctor no-inl L4 41.1 40.9 вызов делегата
6.2 6.2 вызов этого экземпляра 72.6 68.5 new reftype ctor no-inl L5 2.7 2.7 массив sum 1000
5.4 5.4 виртуальный вызов 0,4 0,4 приведение вверх 1 2,8 2,8 массив sum 10000
5.4 5.4 этот виртуальный вызов 0,3 0,3 приведение вниз 0 2,9 2,8 массив sum 100000
6.6 6,5 вызов интерфейса 8,9 8,8 отбрасывает 1 5.6 5.6 массив sum 1000000
1,1 1.0 вызов экземпляра inst itf 9,8 9.7 приведение (вверх 2) вниз 1 3,5 3,5 sum list 1000
0,2 0,2 этот вызов экземпляра ITF 8,9 8,8 отбрасывает 2 6.1 6.1 sum list 10000
5.4 5.4 виртуальный вызов inst itf 8,7 8,6 отбрасывает 3 22,0 22,0 sum list 100000
5.4 5.4 этот виртуальный вызов itf       21,5 21,4 sum list 1000000

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

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

И еще одно заявление об отказе: одно из возвышенных преимуществ доставки компонентов и приложений в виде сборок CIL заключается в том, что ваша программа может автоматически получать быстрее каждую секунду и каждый год — "быстрее каждую секунду", так как среда выполнения может (теоретически) повторно настраивать JIT-скомпилированный код при запуске программы. и "быстрее в год", потому что с каждым новым выпуском среды выполнения более совершенные, интеллектуальные и быстрые алгоритмы могут принимать свежие удары по оптимизации кода. Поэтому если в .NET 1.1 некоторые из этих временных интервалов кажутся менее оптимальными, поймите, что они должны улучшиться в последующих выпусках продукта. Отсюда следует, что любая последовательность машинного кода, указанная в этой статье, может измениться в будущих выпусках платформа .NET Framework.

Заявления об отказе в сторону, данные обеспечивают разумное чувство кишки для текущей производительности различных примитивов. Цифры имеет смысл, и они подтверждают мое утверждение о том, что большинство фрагментированных управляемых кодов выполняется "близко к компьютеру" так же, как скомпилированный машинный код. Примитивные целочисленные и плавающие операции являются быстрыми, вызовы методов различных типов меньше, но (поверьте мне) по-прежнему сопоставимы с собственным C/C++; и все же мы также видим, что некоторые операции, которые обычно являются дешевыми в машинном коде (приведения, хранилища массивов и полей, указатели функций (делегаты)), теперь являются более дорогостоящими. Почему? Давайте посмотрим.

Арифметические операции

Таблица 2. Время арифметических операций (ns)

Avg Min Примитивные Avg Min Примитивные
1.0 1.0 int add 1,3 1,3 float add
1.0 1.0 int sub 1.4 1.4 float sub
2.7 2.7 int mul 2.0 2.0 float mul
35,9 35,7 int div 27,7 27.6 float div
2.1 2.1 int shift      
2.1 2.1 long add 1.5 1.5 double add
2.1 2.1 long sub 1.5 1.5 double sub
34,2 34,1 long mul 2.1 2,0 double mul
50,1 50,0 long div 27,7 27.6 double div
5,1 5,1 длинная смена      

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

Давайте рассмотрим строку фрагментированного кода из целого числа и тестов добавления с плавающей запятой:

Дизассемблировать 1 Int add и float add

int add               a = a + b + c + d + e + f + g + h + i;
0000004c 8B 54 24 10      mov         edx,dword ptr [esp+10h] 
00000050 03 54 24 14      add         edx,dword ptr [esp+14h] 
00000054 03 54 24 18      add         edx,dword ptr [esp+18h] 
00000058 03 54 24 1C      add         edx,dword ptr [esp+1Ch] 
0000005c 03 54 24 20      add         edx,dword ptr [esp+20h] 
00000060 03 D5            add         edx,ebp 
00000062 03 D6            add         edx,esi 
00000064 03 D3            add         edx,ebx 
00000066 03 D7            add         edx,edi 
00000068 89 54 24 10      mov         dword ptr [esp+10h],edx 

float add            i += a + b + c + d + e + f + g + h;
00000016 D9 05 38 61 3E 00 fld         dword ptr ds:[003E6138h] 
0000001c D8 05 3C 61 3E 00 fadd        dword ptr ds:[003E613Ch] 
00000022 D8 05 40 61 3E 00 fadd        dword ptr ds:[003E6140h] 
00000028 D8 05 44 61 3E 00 fadd        dword ptr ds:[003E6144h] 
0000002e D8 05 48 61 3E 00 fadd        dword ptr ds:[003E6148h] 
00000034 D8 05 4C 61 3E 00 fadd        dword ptr ds:[003E614Ch] 
0000003a D8 05 50 61 3E 00 fadd        dword ptr ds:[003E6150h] 
00000040 D8 05 54 61 3E 00 fadd        dword ptr ds:[003E6154h] 
00000046 D8 05 58 61 3E 00 fadd        dword ptr ds:[003E6158h] 
0000004c D9 1D 58 61 3E 00 fstp        dword ptr ds:[003E6158h] 

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

Вызовы методов

В этом разделе мы рассмотрим затраты и реализации вызовов методов. Тестируемый субъект — это класс T , реализующий интерфейс Iс различными видами методов. См. листинг 1.

Листинг 1. Методы теста вызова метода

interface I { void itf1();… void itf5();… }
public class T : I {
    static bool falsePred = false;
    static void dummy(int a, int b, int c, …, int p) { }

    static void inl_s1() { } …    static void s1()     { if (falsePred) dummy(1, 2, 3, …, 16); } …    void inl_i1()        { } …    void i1()            { if (falsePred) dummy(1, 2, 3, …, 16); } …    public virtual void v1() { } …    void itf1()          { } …    virtual void itf5()  { } …}

Рассмотрим таблицу 3. Кажется, при первом приближении метод либо встраиваются (абстракция ничего не стоит) или нет (абстракция стоит >5x целочисленной операции). По-видимому, нет существенной разницы в необработанных затратах на статический вызов, вызов экземпляра, виртуальный вызов или вызов интерфейса.

Таблица 3 Время вызова метода (ns)

Avg Min Примитивные Вызываемая функция Avg Min Примитивные Вызываемая функция
0,2 0,2 встроенный статический вызов inl_s1 5.4 5.4 виртуальный вызов v1
6.1 6.1 статический вызов s1 5.4 5.4 этот виртуальный вызов v1
1,1 1.0 вызов встроенного экземпляра inl_i1 6.6 6,5 вызов интерфейса itf1
6,8 6,8 вызов экземпляра i1 1,1 1.0 вызов экземпляра inst itf itf1
0,2 0,2 inlined this inst call inl_i1 0,2 0,2 этот вызов экземпляра ITF itf1
6.2 6.2 вызов этого экземпляра i1 5.4 5.4 виртуальный вызов inst itf itf5
        5.4 5.4 этот виртуальный вызов itf itf5

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

Давайте подробнее рассмотрим каждый из этих методов.

В первом случае, встроенный статический вызов, мы вызываем ряд пустых статических методов s1_inl() и т. д. Так как компилятор полностью встраивает все вызовы, мы в конечном итоге задаем время пустого цикла.

Чтобы измерить приблизительную стоимость вызова статического метода, мы делаем статические методы s1() и т. д. настолько большими, что их невозможно вставить в вызывающий объект.

Обратите внимание, что нам даже нужно использовать явную переменную falsePredпредиката false . Если бы мы писали

static void s1() { if (false) dummy(1, 2, 3, …, 16); }

JIT-компилятор устранит неактивный вызов dummy и встроит весь (теперь пустой) текст метода, как и раньше. Кстати, здесь некоторые из 6,1 ns времени вызова должны быть отнесены к тесту предиката (false) и перейти в пределах вызываемого статического метода s1. (Кстати, лучший способ отключить встраивание — это CompilerServices.MethodImpl(MethodImplOptions.NoInlining) атрибут.)

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

В дизассемблированном коде 2 в качестве экземпляра используется статическая переменная t , так как при использовании локальной переменной

    T t = new T();

компилятор вытащил экземпляр NULL проверка из цикла.

Дизассемблировать 2. Сайт вызова метода экземпляра с пустым экземпляром "проверка"

               t.i1();
00000012 8B 0D 30 21 A4 05 mov         ecx,dword ptr ds:[05A42130h] 
00000018 39 09             cmp         dword ptr [ecx],ecx 
0000001a E8 C1 DE FF FF    call        FFFFDEE0 

Варианты встраиваемого вызова этого экземпляра и вызова этого экземпляра одинаковы, за исключением thisэкземпляра ; здесь значение null проверка было устранено.

Дизассемблация 3. Этот сайт вызова метода экземпляра

               this.i1();
00000012 8B CE            mov         ecx,esi
00000014 E8 AF FE FF FF   call        FFFFFEC8

Вызовы виртуальных методов работают так же, как и в традиционных реализациях C++. Адрес каждого вновь введенного виртуального метода хранится в новом слоте в таблице методов типа. Таблица методов каждого производного типа соответствует и расширяет таблицу его базового типа, а любое переопределение виртуального метода заменяет адрес виртуального метода базового типа адресом виртуального метода производного типа адресом виртуального метода в соответствующем слоте в таблице методов производного типа.

На сайте вызова вызов виртуального метода выполняет две дополнительные нагрузки по сравнению с вызовом экземпляра: один для получения адреса таблицы метода (всегда находится по адресу *(this+0)), а другой — для получения соответствующего адреса виртуального метода из таблицы методов и его вызова. См. дизассемблию 4.

Дизассемблировать 4 Сайт вызова виртуального метода

               this.v1();
00000012 8B CE            mov         ecx,esi 
00000014 8B 01            mov         eax,dword ptr [ecx] ; fetch method table address
00000016 FF 50 38         call        dword ptr [eax+38h] ; fetch/call method address

Наконец, мы перейдем к вызовам методов интерфейса (дизассембл. 5). Они не имеют точного эквивалента в C++. Любой тип может реализовать любое количество интерфейсов, и для каждого интерфейса логически требуется собственная таблица методов. Для отправки в метод интерфейса мы ищем таблицу методов, ее карту интерфейса, запись интерфейса в этой карте, а затем вызываем косвенный вызов через соответствующую запись в разделе интерфейса таблицы методов.

Сайт вызова метода интерфейса дизассемблирования 5

               i.itf1();
00000012 8B 0D 34 21 A4 05 mov        ecx,dword ptr ds:[05A42134h]; instance address
00000018 8B 01             mov        eax,dword ptr [ecx]         ; method table addr
0000001a 8B 40 0C          mov        eax,dword ptr [eax+0Ch]     ; interface map addr
0000001d 8B 40 7C          mov        eax,dword ptr [eax+7Ch]     ; itf method table addr
00000020 FF 10             call       dword ptr [eax]             ; fetch/call meth addr

Оставшаяся часть примитивного времени, вызов экземпляра inst itf, этот вызов экземпляра ITF, виртуальный вызов inst itf, этот виртуальный вызов itf подчеркивают идею о том, что всякий раз, когда метод производного типа реализует метод интерфейса, он остается доступным для вызова через сайт вызова метода экземпляра.

Например, при тестировании этого вызова экземпляра ITF при вызове реализации метода интерфейса через ссылку на экземпляр (не интерфейс) метод интерфейса успешно встраивается, а затраты идут до 0 нс. Даже реализация метода интерфейса потенциально может быть встраиваемой при вызове ее в качестве метода экземпляра.

Вызовы методов, которые еще не будут jitted

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

Если вызываемый (целевой метод) еще не был обособлен, компилятор выдает косвенный вызов через указатель, который сначала инициализируется с помощью заглушки prejit. Первый вызов целевого метода поступает в заглушку, которая запускает JIT-компиляцию метода, создает машинный код и обновляет указатель для обращения к новому машинного кода.

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

Создание нового объекта

Создание нового объекта состоит из двух этапов: выделение объектов и инициализация объекта.

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

Для типичных небольших объектов ссылочного типа выделение кучи выполняется очень быстро. После каждой сборки мусора, за исключением закрепленных объектов, живые объекты из кучи поколения 0 сжимаются и преобразуются в поколение 1, поэтому распределитель памяти имеет хорошую большую непрерывную область свободной памяти для работы. Большинство выделений объектов влечет за собой только приращение указателя и границы проверка, что дешевле, чем обычный распределитель свободного списка C/C++ (malloc/operator new). Сборщик мусора даже учитывает размер кэша компьютера, чтобы попытаться сохранить объекты поколения 0 в быстром месте иерархии кэша и памяти.

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

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

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

Конечно, амортизированная стоимость сборки мусора большого кратковременного объекта больше, чем стоимость небольшого краткоживующего объекта. Каждое выделение объекта приближает нас к следующему циклу сборки мусора; Более крупные объекты делают так, что гораздо раньше, чем маленькие. Рано (или позже) наступит момент расплаты. Циклы сборки мусора, особенно коллекции поколения 0, выполняются очень быстро, но не являются свободными, даже если подавляющее большинство новых объектов не работает: чтобы найти (пометить) динамические объекты, сначала необходимо приостановить потоки, а затем пройти по стеку и другим структурам данных для сбора ссылок на корневые объекты в кучу.

(Что еще важнее, меньше крупных объектов помещается в тот же объем кэша, что и небольшие объекты. Эффекты промахов кэша могут легко доминировать в эффектах длины пути кода.)

После выделения пространства для объекта остается инициализировать его (создать). Среда CLR гарантирует, что все ссылки на объекты предварительно инициализированы до null, а все примитивные скалярные типы инициализируются 0, 0,0, false и т. д. (Поэтому не нужно избыточно делать это в определяемых пользователем конструкторах. Конечно, не стесняйтесь. Но имейте в виду, что JIT-компилятор в настоящее время не обязательно оптимизирует избыточные хранилища.)

Помимо обнуления полей экземпляра, среда CLR инициализирует (только ссылочные типы) внутренние поля реализации объекта: указатель таблицы метода и слово заголовка объекта, которое предшествует указателю таблицы метода. Массивы также получают поле Length, а массивы объектов — поля Length и типа элемента.

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

Теоретически это может быть дорогостоящим для сценариев глубокого наследования. Если E расширяет D, расширяет C, B расширяет A (расширяет System.Object), то инициализация E всегда будет вызывать пять вызовов методов. На практике все не так уж плохо, так как компилятор встраивает (в пустоту) вызовы пустых конструкторов базовых типов.

Обращаясь к первому столбцу таблицы 4, мы можем создать и инициализировать структуру D с четырьмя полями int в 8 int-add-times. Дизассемблированный код 6 — это код, созданный из трех разных циклов времени, создающих A, C и E. (В каждом цикле мы изменяем каждый новый экземпляр, чтобы JIT-компилятор не оптимизировал все.)

Таблица 4 Значение и время создания объекта ссылочного типа (ns)

Avg Min Примитивные Avg Min Примитивные Avg Min Примитивные
2.6 2.6 новый valtype L1 22,0 20,3 new reftype L1 22,9 20,7 new rt ctor L1
4.6 4.6 новый вальтип L2 26,1 23,9 new reftype L2 27.8 25.4 new rt ctor L2
6.4 6.4 новый valtype L3 30,2 27,5 new reftype L3 32,7 29,9 new rt ctor L3
8.0 8.0 новый valtype L4 34,1 30.8 новый ссылочный тип L4 37.7 34,1 new rt ctor L4
23,0 22,9 новый valtype L5 39.1 34.4 new reftype L5 43.2 39.1 new rt ctor L5
      22,3 20,3 new rt empty ctor L1 28,6 26,7 new rt no-inl L1
      26,5 23,9 new rt empty ctor L2 38.9 36,5 new rt no-inl L2
      38.1 34.7 new rt empty ctor L3 50.6 47.7 new rt no-inl L3
      34.7 30,7 new rt empty ctor L4 61.8 58.2 new rt no-inl L4
      38.5 34.3 new rt empty ctor L5 72.6 68.5 new rt no-inl L5

Дизассемблировать 6. Создание объекта типа значения

               A a1 = new A(); ++a1.a;
00000020 C7 45 FC 00 00 00 00 mov     dword ptr [ebp-4],0 
00000027 FF 45 FC         inc         dword ptr [ebp-4] 

               C c1 = new C(); ++c1.c;
00000024 8D 7D F4         lea         edi,[ebp-0Ch] 
00000027 33 C0            xor         eax,eax 
00000029 AB               stos        dword ptr [edi] 
0000002a AB               stos        dword ptr [edi] 
0000002b AB               stos        dword ptr [edi] 
0000002c FF 45 FC         inc         dword ptr [ebp-4] 

               E e1 = new E(); ++e1.e;
00000026 8D 7D EC         lea         edi,[ebp-14h] 
00000029 33 C0            xor         eax,eax 
0000002b 8D 48 05         lea         ecx,[eax+5] 
0000002e F3 AB            rep stos    dword ptr [edi] 
00000030 FF 45 FC         inc         dword ptr [ebp-4] 

Следующие пять таймингов (новый рефтип L1, ... new reftype L5) предназначены для пяти уровней наследования ссылочных типов A, ..., E, без определяемых пользователем конструкторов:

    public class A     { int a; }
    public class B : A { int b; }
    public class C : B { int c; }
    public class D : C { int d; }
    public class E : D { int e; }

Сравнивая время ссылочного типа с временем типа значения, мы видим, что амортизированное выделение и освобождение каждого экземпляра составляет приблизительно 20 нс (20X добавочного времени) на тестовом компьютере. Это быстро — выделение, инициализация и восстановление около 50 миллионов кратковременных объектов в секунду, устойчивых. Для объектов размером до пяти полей на выделение и сбор приходится только половина времени создания объекта. См. дизассемблию 7.

Дизассемблированное 7 Создание объекта ссылочного типа

               new A();
0000000f B9 D0 72 3E 00   mov         ecx,3E72D0h 
00000014 E8 9F CC 6C F9   call        F96CCCB8 

               new C();
0000000f B9 B0 73 3E 00   mov         ecx,3E73B0h 
00000014 E8 A7 CB 6C F9   call        F96CCBC0 

               new E();
0000000f B9 90 74 3E 00   mov         ecx,3E7490h 
00000014 E8 AF CA 6C F9   call        F96CCAC8 

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

  1. New rt empty ctor L1, ..., new rt empty ctor L5: Каждый тип A, ..., E имеет пустой пользовательский конструктор. Все они встраиваются, и созданный код совпадает с приведенным выше.

  2. New rt ctor L1, ..., new rt ctor L5: Каждый тип A, ..., имеет определяемый пользователем конструктор, E который задает переменной экземпляра значение 1:

        public class A     { int a; public A() { a = 1; } }
        public class B : A { int b; public B() { b = 1; } }
        public class C : B { int c; public C() { c = 1; } }
        public class D : C { int d; public D() { d = 1; } }
        public class E : D { int e; public E() { e = 1; } }
    

Компилятор встраивает в сайт каждый набор вызовов new конструктора вложенного базового класса. (Дизассембл. 8).

Дизассемблированные конструкторы 8 Глубоко встраиваются в унаследованные конструкторы

               new A();
00000012 B9 A0 77 3E 00   mov         ecx,3E77A0h 
00000017 E8 C4 C7 6C F9   call        F96CC7E0 
0000001c C7 40 04 01 00 00 00 mov     dword ptr [eax+4],1 

               new C();
00000012 B9 80 78 3E 00   mov         ecx,3E7880h 
00000017 E8 14 C6 6C F9   call        F96CC630 
0000001c C7 40 04 01 00 00 00 mov     dword ptr [eax+4],1 
00000023 C7 40 08 01 00 00 00 mov     dword ptr [eax+8],1 
0000002a C7 40 0C 01 00 00 00 mov     dword ptr [eax+0Ch],1 

               new E();
00000012 B9 60 79 3E 00   mov         ecx,3E7960h 
00000017 E8 84 C3 6C F9   call        F96CC3A0 
0000001c C7 40 04 01 00 00 00 mov     dword ptr [eax+4],1 
00000023 C7 40 08 01 00 00 00 mov     dword ptr [eax+8],1 
0000002a C7 40 0C 01 00 00 00 mov     dword ptr [eax+0Ch],1 
00000031 C7 40 10 01 00 00 00 mov     dword ptr [eax+10h],1 
00000038 C7 40 14 01 00 00 00 mov     dword ptr [eax+14h],1 
  1. New rt no-inl L1, ..., new rt no-inl L5: Каждый тип A, ..., E имеет определяемый пользователем конструктор, который был намеренно записан как слишком дорогой для встроенной. Этот сценарий имитирует затраты на создание сложных объектов с иерархиями глубокого наследования и конструкторами largish.

      public class A     { int a; public A() { a = 1; if (falsePred) dummy(…); } }
      public class B : A { int b; public B() { b = 1; if (falsePred) dummy(…); } }
      public class C : B { int c; public C() { c = 1; if (falsePred) dummy(…); } }
      public class D : C { int d; public D() { d = 1; if (falsePred) dummy(…); } }
      public class E : D { int e; public E() { e = 1; if (falsePred) dummy(…); } }
    

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

Interlude: CLR Profiler Demo

Теперь для краткой демонстрации профилировщика CLR. Профилировщик CLR, ранее известный как профилировщик выделения, использует API-интерфейсы профилирования CLR для сбора данных о событиях, в частности вызовов, возвратов и событий выделения объектов и сборки мусора, при запуске приложения. (Профилировщик CLR является "инвазивным" профилировщиком, что, к сожалению, существенно замедляет профилированное приложение.) После сбора событий вы используете CLR Profiler для изучения выделения памяти и поведения сборки мусора приложения, включая взаимодействие между иерархическим графом вызовов и шаблонами выделения памяти.

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

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

На рисунке 1 показано временная шкала представление кучи во время выполнения драйвера проверки времени. Шаблон пилы указывает на выделение многих тысяч экземпляров объектов C (пурпурная), D (фиолетовая) и E (синяя). Каждые несколько миллисекунд мы разжевываем еще около 150 КБ ОЗУ в куче нового объекта (поколение 0), и сборщик мусора запускается ненадолго, чтобы перезаработать его и повысить уровень всех живых объектов до поколения 1. Примечательно, что даже в этой инвазивной (медленной) среде профилирования, в интервале 100 мс (2,8 с до 2,9 с), мы подвергаемся ~8 циклам 0 GC поколения 0. Затем при 2,977 с, что освободить место для другого E экземпляра, сборщик мусора выполняет сборку мусора поколения 1, которая собирает и сжимает кучу поколения 1, и поэтому пила продолжается с более низкого начального адреса.

Рис. 1. Представление временной линии профилировщика CLR

Обратите внимание, что чем больше объект (E больше, чем D больше C), тем быстрее заполняется куча поколения 0 и тем чаще выполняется цикл сборки мусора.

Приведения и проверки типов экземпляров

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

Таблица 5 Время приведения и isinst (ns)

Avg Min Примитивные Avg Min Примитивные
0,4 0,4 приведение 1 0,8 0,8 isinst up 1
0,3 0,3 приведение вниз 0 0,8 0,8 isinst down 0
8,9 8,8 отбрасывает 1 6.3 6.3 isinst down 1
9,8 9.7 приведение (вверх 2) вниз 1 10,7 10.6 isinst (up 2) down 1
8,9 8,8 отбрасывает 2 6.4 6.4 isinst down 2
8,7 8,6 отбрасывает 3 6.1 6.1 isinst down 3

В таблице 5 показаны затраты на эти обязательные проверки типов. Приведение из производного типа к базовому типу всегда безопасно и бесплатно; в то время как приведение от базового типа к производному типу должно быть проверено на тип.

Приведение (с флажком) преобразует ссылку на объект в целевой тип или вызывает исключение InvalidCastException.

В отличие от этого, isinst инструкция CIL используется для реализации ключевое слово C# as :

bac = ac as B;

Если ac не B является или производным от B, результатом является null, а не исключение.

В листинге 2 показан один из циклов времени приведения, а дизассемблированный 9 — созданный код для одного приведения к производном типу. Для выполнения приведения компилятор выдает прямой вызов вспомогательной подпрограммы.

Листинг 2. Цикл для тестирования времени приведения

public static void castUp2Down1(int n) {
    A ac = c; B bd = d; C ce = e; D df = f;
    B bac = null; C cbd = null; D dce = null; E edf = null;
    for (n /= 8; --n >= 0; ) {
        bac = (B)ac; cbd = (C)bd; dce = (D)ce; edf = (E)df;
        bac = (B)ac; cbd = (C)bd; dce = (D)ce; edf = (E)df;
    }
}

Дизассемблированное 9 Приведение вниз

               bac = (B)ac;
0000002e 8B D5            mov         edx,ebp 
00000030 B9 40 73 3E 00   mov         ecx,3E7340h 
00000035 E8 32 A7 4E 72   call        724EA76C 

Свойства

В управляемом коде свойство — это пара методов, метод получения свойства и метод задания свойств, которые действуют как поле объекта . Метод get_ извлекает свойство ; Метод set_ обновляет свойство до нового значения.

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

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

Таблица 6 Время полей и свойств (ns)

Avg Min Примитивные
1.0 1.0 Поле get
1,2 1,2 get prop
1,2 1,2 поле set
1,2 1,2 set prop
6.4 6.3 получение виртуальной опоры
6.4 6.3 set virtual prop

Барьеры записи

Сборщик мусора CLR использует хорошие преимущества "гипотезы поколений" ( большинство новых объектов умирают молодыми), чтобы свести к минимуму затраты на сбор данных.

Куча логически секционирована на поколения. Новейшие объекты живут в поколении 0 (поколение 0). Эти объекты еще не сохранились в коллекции. Во время коллекции gen 0 сборка мусора определяет, какие объекты 0-го поколения доступны из корневого набора сборки мусора, который включает ссылки на объекты в регистрах компьютеров, в стеке, ссылки на статические объекты поля класса и т. д. Транзитивно доступные объекты являются "живыми" и повышаются (копируются) до поколения 1.

Так как общий размер кучи может составлять сотни МБ, в то время как размер кучи 0-го поколения может составлять всего 256 КБ, ограничение масштаба трассировки графа объектов сборки мусора до кучи 0-го поколения является оптимизацией, необходимой для достижения очень короткого времени приостановки сбора данных в среде CLR.

Однако можно сохранить ссылку на объект поколения 0 в поле ссылки объекта 1-го или 2-го поколения. Так как мы не сканируем объекты 1-го или 2-го поколения во время коллекции 0-го поколения, если это единственная ссылка на данный объект поколения 0, этот объект может быть ошибочно освобожден сборкой мусора. Мы не можем этого допустить!

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

Накладные расходы на запись в хранилище на объект-ссылку на поле сравнимы со стоимостью простого вызова метода (таблица 7). Это новые затраты, которые отсутствуют в собственном коде C/C++, но обычно это небольшая цена за супербыстрое выделение объектов и сборку мусора, а также многочисленные преимущества производительности автоматического управления памятью.

Таблица 7. Время барьера записи (ns)

Avg Min Примитивные
6.4 6.4 Барьер записи

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

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

Доступ к элементам массива

Для диагностики и исключения ошибок массива вне границ и повреждений кучи, а также для защиты целостности самой среды CLR проверяются границы элементов массива и проверяются границы, гарантируя, что индекс находится в пределах интервала [0,array. Длина 1] включительно или исключение IndexOutOfRangeException.

Наши тесты измеряют время загрузки или хранения элементов int[] массива и массива A[] . (Таблица 8).

Таблица 8 Время доступа к массиву (ns)

Avg Min Примитивные
1,9 1,9 load int array elem
1,9 1,9 store int array elem
2,5 2,5 load obj array elem
16,0 16,0 store obj array elem

Границы проверка требуют сравнения индекса массива с неявным массивом. Поле Длины. Как показывает дизассемблирование 10, всего в двух инструкциях мы проверка, что индекс не меньше 0, не больше массива или не равен ей. Длина — если это так, мы ветвимся в последовательность вне строк, которая вызывает исключение. То же самое относится и к нагрузкам элементов массива объектов, а также для хранилищ в массивах ints и других простых типов значений. (Load obj array elem time (незначительно) медленнее из-за небольшой разницы во внутреннем цикле.)

Дизассемблированное 10. Загрузка элемента массива int

                          ; i in ecx, a in edx, sum in edi
               sum += a[i];
00000024 3B 4A 04         cmp         ecx,dword ptr [edx+4] ; compare i and array.Length
00000027 73 19            jae         00000042 
00000029 03 7C 8A 08      add         edi,dword ptr [edx+ecx*4+8] 
…                         ; throw IndexOutOfRangeException
00000042 33 C9            xor         ecx,ecx 
00000044 E8 52 78 52 72   call        7252789B 

Благодаря оптимизации качества кода JIT-компилятор часто устраняет избыточные проверки границ.

Вспоминая предыдущие разделы, можно ожидать, что хранилища элементов массива объектов будут значительно дороже. Чтобы сохранить ссылку на объект в массиве ссылок на объекты, среда выполнения должна:

  1. проверка индекс массива находится в границах;
  2. проверка объект является экземпляром типа элемента массива;
  3. выполнение барьера записи (замечая ссылку на объект межпорождения из массива в объект).

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

Элемент массива объектов Disassembly 11 Store

                          ; objarray in edi
                          ; obj      in ebx
               objarray[1] = obj;
00000027 53               push        ebx  
00000028 8B CF            mov         ecx,edi 
0000002a BA 01 00 00 00   mov         edx,1 
0000002f E8 A3 A0 4A 72   call        724AA0D7   ; store object array element helper

Упаковка–преобразование и распаковка–преобразование

Партнерство между компиляторами .NET и средой CLR позволяет использовать типы значений, включая примитивные типы, такие как int (System.Int32), как если бы они были ссылочными типами, для обращения в качестве ссылок на объекты. Эта возможность — синтаксический сахар — позволяет передавать типы значений в методы как объекты, храниться в коллекциях в виде объектов и т. д.

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

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

Как показано в таблице 9 (в сравнении с таблицей 4), амортизированное время, необходимое для поля int и последующего сбора мусора, сравнимо со временем, необходимым для создания экземпляра небольшого класса с одним полем int.

Таблица 9. Время ввода в поле и распаковку (ns)

Avg Min Примитивные
29,0 21,6 box int
3,0 3,0 распаковка int

Для распаковки упаковаемого объекта int требуется явное приведение к int. Он компилируется в сравнение типа объекта (представленного адресом таблицы методов) и прямоугольного адреса таблицы метода int. Если они равны, значение копируется из объекта . В противном случае возникает исключение. См. дизассемблированное 12.

Дизассемблировать 12 Box и распаковка int

box               object o = 0;
0000001a B9 08 07 B9 79   mov         ecx,79B90708h 
0000001f E8 E4 A5 6C F9   call        F96CA608 
00000024 8B D0            mov         edx,eax 
00000026 C7 42 04 00 00 00 00 mov         dword ptr [edx+4],0 

unbox               sum += (int)o;
00000041 81 3E 08 07 B9 79 cmp         dword ptr [esi],79B90708h ; "type == typeof(int)"?
00000047 74 0C            je          00000055 
00000049 8B D6            mov         edx,esi 
0000004b B9 08 07 B9 79   mov         ecx,79B90708h 
00000050 E8 A9 BB 4E 72   call        724EBBFE                   ; no, throw exception
00000055 8D 46 04         lea         eax,[esi+4]
00000058 3B 08            cmp         ecx,dword ptr [eax] 
0000005a 03 38            add         edi,dword ptr [eax]        ; yes, fetch int field

Делегаты

В C указатель на функцию является примитивным типом данных, в котором буквально хранится адрес функции.

C++ добавляет указатели на функции-члены. Указатель на функцию-член (PMF) представляет отложенный вызов функции-члена. Адрес не виртуальной функции-члена может быть простым адресом кода, но адрес виртуальной функции-члена должен содержать определенный вызов функции виртуального члена. Разыменовка такого PMF является вызовом виртуальной функции.

Чтобы разыменовать PMF C++, необходимо предоставить экземпляр:

    A* pa = new A;
    void (A::*pmf)() = &A::af;
    (pa->*pmf)();

Много лет назад в команде разработчиков компиляторов Visual C++ мы задавали себе вопрос о том, какой тип beastie является голым выражением pa->*pmf (оператор вызова функции без функции)? Мы назвали это привязанным указателем на функцию-член , но скрытый вызов функции-члена так же, как apt.

Возвращаясь к управляемому коду, объект делегата — это просто вызов скрытого метода. Объект делегата представляет как метод для вызова, так и экземпляр для вызова, или для делегата статического метода, только статический метод для вызова.

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

Типы делегатов в C# являются производными типами MulticastDelegate. Этот тип предоставляет широкие возможности семантики, включая возможность создания списка вызовов пар (объект и метод), которые будут вызываться при вызове делегата.

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

Все эти богатые семантики не являются недорогими. Сравнивая таблицы 10 и 3, обратите внимание, что вызов делегата ** примерно в восемь раз медленнее, чем вызов метода. Ожидается, что это улучшится с течением времени.

Таблица 10 Время вызова делегата (ns)

Avg Min Примитивные
41.1 40.9 Вызов делегата

Промахов кэша, ошибок страниц и архитектуры компьютера

Еще в "старые добрые времена", около 1983 года, процессоры были медленными (~5 миллионов инструкций в секунду), и, условно говоря, ОЗУ была достаточно быстрой, но небольшой (~300 ns время доступа на 256 КБ DRAM), а диски были медленными и большими (~25 мс времени доступа на дисках по 10 МБ). Микропроцессоры ПК были скалярными CISC, большинство с плавающей запятой находились в программном обеспечении, и кэши не были.

После еще двадцати лет закона Мура, около 2003 года, процессоры являются быстрыми (выдавая до трех операций в цикле с частотой 3 ГГц), ОЗУ относительно очень медленно (около 100 нс времени доступа на 512 МБ DRAM), а диски ледников медленно и огромны (~10 мс времени доступа на дисках 100 ГБ). Микропроцессоры ПК теперь являются неупорядоченными потоками данных сверхскаларовых гиперпоточности RISCs (выполняют декодированные инструкции CISC), и есть несколько уровней кэша. Например, определенный серверный микропроцессор имеет кэш данных уровня 32 КБ (возможно, 2 цикла задержки), 512 КБ кэша данных L2 и 2 МБ кэша данных L3 (возможно, дюжина циклов задержки). все на чипе.

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

Теперь наши самые быстрые компьютеры могут выполнять до 9000 операций на микросекунд, но в том же микросекундах загружаются или хранятся в DRAM ~10 строк кэша. В кругах компьютерной архитектуры это называется попаданием в стену памяти. Кэши скрывают задержку памяти, но только до точки. Если код или данные не помещаются в кэш и (или) демонстрируют плохое расположение ссылок, наша сверхзвуковая струя 9000 операций на микросекунд вырождается до 10 нагрузки на микросекунд трицикла.

И (не позволяйте этому случиться) если рабочий набор программы превышает доступную физическую память, и программа начинает принимать жесткие ошибки страницы, то в каждой 10 000-микросекундной службе сбоя страницы (доступ к диску), мы упускаем возможность довести пользователя до 90 миллионов операций ближе к их ответу. Это просто настолько ужасно, что я надеюсь, что с этого дня вы будете заботиться о том, чтобы измерить ваш рабочий набор (vadump) и использовать такие инструменты, как CLR Profiler, чтобы исключить ненужные выделения и случайное хранение графа объектов.

Но как все это связано с знанием стоимости примитивов управляемого кода?Все*.*

Возвращаясь к таблице 1, сводному списку времени примитивов управляемого кода, измеряемому на P-III с частотой 1,1 ГГц, обратите внимание, что каждый раз, даже амортизированные затраты на выделение, инициализацию и восстановление пяти объектов поля с пятью уровнями явных вызовов конструктора , быстрее, чем один доступ к DRAM. Одна загрузка, которая пропускает все уровни встроенного кэша, может занять больше времени, чем почти любая отдельная операция с управляемым кодом.

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

Время для простой демонстрации: быстрее ли суммировать массив ints или суммировать эквивалентный связанный список ints? Что, сколько и почему?

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

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

Дизассемблированное число 13 Массив суммы int и связанный список sum int

sum int array:            sum += a[i];
00000024 3B 4A 04         cmp         ecx,dword ptr [edx+4]       ; bounds check
00000027 73 19            jae         00000042 
00000029 03 7C 8A 08      add         edi,dword ptr [edx+ecx*4+8] ; load array elem
               for (int i = 0; i < m; i++)
0000002d 41               inc         ecx  
0000002e 3B CE            cmp         ecx,esi 
00000030 7C F2            jl          00000024 


sum int linked list:         sum += l.item; l = l.next;
0000002a 03 70 08         add         esi,dword ptr [eax+8] 
0000002d 8B 40 04         mov         eax,dword ptr [eax+4] 
               sum += l.item; l = l.next;
00000030 03 70 08         add         esi,dword ptr [eax+8] 
00000033 8B 40 04         mov         eax,dword ptr [eax+4] 
               sum += l.item; l = l.next;
00000036 03 70 08         add         esi,dword ptr [eax+8] 
00000039 8B 40 04         mov         eax,dword ptr [eax+4] 
               sum += l.item; l = l.next;
0000003c 03 70 08         add         esi,dword ptr [eax+8] 
0000003f 8B 40 04         mov         eax,dword ptr [eax+4] 
               for (m /= 4; --m >= 0; ) {
00000042 49               dec         ecx  
00000043 85 C9            test        ecx,ecx 
00000045 79 E3            jns         0000002A 

Ссылаясь на Дизассемблированное 13, я сложил палубу в пользу связанного списка обхода, распространив его четыре раза, даже удалив обычный пустой указатель конца списка проверка. Каждому элементу в цикле массива требуется шесть инструкций, тогда как каждому элементу в цикле связанного списка требуется только 11/4 = 2,75 инструкций. Теперь что быстрее?

Условия теста: сначала создайте массив из одного миллиона ints и простой традиционный связанный список из одного миллиона ints (1 M узлов списка). Затем время, необходимое для сложения первых 1 000, 10 000, 100 000 и 1 000 000 элементов. Повторяйте каждый цикл много раз, чтобы измерить наиболее лестную работу кэша для каждого случая.

Что быстрее? После того как вы угадали, ознакомьтесь с ответами: последние восемь записей в таблице 1.

Интересно. Время становится значительно медленнее, так как указанные данные увеличиваются по сравнению с последовательными размерами кэша. Версия массива всегда быстрее, чем версия связанного списка, даже если она выполняет в два раза больше инструкций; Для 100 000 элементов версия массива в семь раз быстрее!

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

В случае с 100 000 элементов процессор тратит (в среднем) приблизительно (22–3,5)/22 = 84 % своего времени в ожидании строки кэша узла списка для чтения из DRAM. Это звучит плохо, но все может быть гораздо хуже. Так как связанные элементы списка небольшие, многие из них помещаются в строку кэша. Так как мы просматриваем список в порядке выделения и сборщик мусора сохраняет порядок распределения даже при сжатии мертвых объектов из кучи, вполне вероятно, что после получения одного узла в строке кэша следующие несколько узлов теперь также находятся в кэше. Если узлы были больше или узлы списка находились в случайном порядке, то каждый посещаемый узел вполне может быть полным промахом кэша. Добавление 16 байт к каждому узлу списка удваивает время обхода на элемент до 43 нс; +32 байта, 67 нс/элемент; и при добавлении 64 байт снова удвоится значение до 146 нс/элемент, что, вероятно, составляет среднюю задержку DRAM на тестовом компьютере.

Так что же здесь урок на вынос? Избегайте связанных списков из 100 000 узлов? Нет. Урок заключается в том, что влияние кэша может доминировать в любом аспекте низкой эффективности управляемого кода по сравнению с машинным кодом. Если вы пишете критически важный для производительности управляемый код, в частности код, который управляет большими структурами данных, учитывайте эффекты кэша, продумайте шаблоны доступа к структуре данных и стремитесь к меньшему объему данных и хорошему месту ссылки.

Кстати, тенденция заключается в том, что стенка памяти, соотношение времени доступа DRAM, разделенное на время работы ЦП, будет продолжать расти с течением времени.

Ниже приведены некоторые правила разработки с вниманием к кэшу:

  • Экспериментируйте и измеряйте свои сценарии, так как трудно предсказать эффекты второго порядка и потому, что правила большого пальца не стоят бумаги, на которых они напечатаны.
  • Некоторые структуры данных, на примером с помощью массивов, используют неявную смежность для представления связи между данными. Другие, на пример со связанными списками, используют явные указатели (ссылки) для представления отношения. Неявная смежность обычно предпочтительнее— "неявность" экономит место по сравнению с указателями; и смежность обеспечивают стабильное расположение ссылок и могут позволить процессору начать больше работы, прежде чем переходить к следующему указателю.
  • Некоторые шаблоны использования предпочитают гибридные структуры — списки небольших массивов, массивы массивов или B-деревья.
  • Возможно, алгоритмы планирования с учетом доступа к диску, разработанные в том случае, когда доступ к диску стоит всего 50 000 инструкций ЦП, должны быть переработаны теперь, когда для доступа К DRAM могут потребоваться тысячи операций ЦП.
  • Так как сборщик мусора clR mark-and-compact сохраняет относительный порядок объектов, объекты, выделенные вместе во времени (и в одном потоке), как правило, остаются вместе в пространстве. Вы можете использовать это явление, чтобы вдумчиво распределить данные о кликах в общих строках кэша.
  • Возможно, вы захотите разделить данные на горячие части, которые часто обрабатываются и должны помещаться в кэш, и на холодные части, которые редко используются и могут быть "кэшированы".

Эксперименты со временем "Сделай сам"

Для измерения времени в этой статье я использовал счетчик QueryPerformanceCounter производительности Win32 с высоким разрешением (и QueryPerformanceFrequency).

Они легко вызываются через P/Invoke:

    [System.Runtime.InteropServices.DllImport("KERNEL32")]
    private static extern bool QueryPerformanceCounter(
        ref long lpPerformanceCount);

    [System.Runtime.InteropServices.DllImport("KERNEL32")]
    private static extern bool QueryPerformanceFrequency(
        ref long lpFrequency);

Вы вызываете QueryPerformanceCounter до и сразу после цикла времени, вычитаете счетчики, умножаете на 1,0e9, делите на частоту, делите на количество итераций, и это приблизительное время для каждой итерации в ns.

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

Кстати, я создал дизассемблии в этой статье с помощью окна дизассемблирования в VS.NET 2003 году. Однако в этом есть трюк. Если вы запускаете приложение в отладчике VS.NET даже в качестве оптимизированного исполняемого файла, встроенного в режиме выпуска, оно будет выполняться в режиме отладки, в котором отключаются такие оптимизации, как встраивание. Единственный способ получить представление об оптимизированном машинном коде, который выдает JIT-компилятор, — запустить тестовое приложение за пределами отладчика, а затем подключиться к нему с помощью Debug.Processes.Attach.

Модель затрат на пространство?

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

Вопросы низкого уровня (некоторые из которых относятся к C# (по умолчанию TypeAttributes.SequentialLayout) и x86:

  • Размер типа значения обычно представляет собой общий размер его полей, при этом поля размером 4 байта или меньше выровнены по естественным границам.
  • Для реализации объединений можно использовать [StructLayout(LayoutKind.Explicit)] атрибуты и [FieldOffset(n)] .
  • Размер ссылочного типа составляет 8 байт плюс общий размер полей, округленных до следующей 4-байтовой границы и 4-байтовых или меньших полей, выровненных по естественным границам.
  • В C# объявления перечисления могут указывать произвольный целочисленный базовый тип (кроме char), поэтому можно определить 8-разрядные, 16-разрядные, 32- и 64-разрядные перечисления.
  • Как и в C/C++, вы часто можете сбрить несколько десятков процентов пространства от большого объекта, соответствующим образом определив размер целочисленных полей.
  • Размер выделенного ссылочного типа можно проверить с помощью профилировщика CLR.
  • Управление большими объектами (много десятков КБ или более) осуществляется в отдельной куче больших объектов, что исключает возможность дорогостоящего копирования.
  • Завершаемые объекты принимают дополнительное поколение сборки мусора для восстановления— используйте их экономно и рассмотрите возможность использования шаблона dispose.

Общие рекомендации:

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

Отражение

Было сказано, что "если вы должны спросить, что отражение стоит, вы не можете себе это позволить". Если вы читали это далеко, вы знаете, насколько важно спросить, что стоит, и измерить эти затраты.

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

Заключение

Теперь вы знаете (более или менее) затраты на управляемый код на самом низком уровне. Теперь у вас есть базовые знания, необходимые для более эффективной реализации компромиссов и более быстрого написания управляемого кода.

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

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

Что касается команды CLR, мы продолжаем работать над тем, чтобы предоставить платформу, которая значительно более эффективна, чем машинный код , и тем не менее быстрее, чем машинный код. Ожидайте, что все будет лучше и лучше. Следите за новостями.

Помните свое обещание.

Ресурсы