Вы думаете, что ваша операционная система должна быть надежной.

Введение

Майк Делиман был порядком занят в прошлом январе (статья вышла в октябре 2004 года — прим. перев.), когда марсоход Спирит испытывал проблемы с памятью и каналами связи вскоре после посадки на Красной Планете. Он является членом команды в компании Wind River Systems, которая создала операционную систему, работающую в самом сердце марсоходов; и он был одним из тех людей, которые работали круглосуточно, чтобы найти причину и решить проблему, которая таинственным образом приостановила выполнение марсианской миссии.

Делиман работает главным инженером операционных систем в компании Wind River Systems. После окончания Калифорнийского университета в Санта-Круз, со специализацией в области компьютерных и информационных наук, он пошел работать в компанию, работающую с Unix, где и познакомился с VxWorks, операционной системой реального времени производства компании Wind River, адаптированную впоследствии для использования в марсоходах. "Я был очень впечатлен ранней версией VxWorks", — говорит он. "И так сложилось, что буквально через несколько лет после начала моей работы, компания закрыла свои офисы в Сан-Хосе, и я перешел в Wind River". C тех пор он работал над различными космическими проектами вместе с лабораторией реактивных двигателей НАСА.

Беседу с Делиманом о роли программного обеспечения в космосе ведет Джордж Невилл-Нейл, который также знаком с VxWorks. Он разработал модель драйверов устройств для сетевого взаимодействия, которая используется в VxWorks, работал над модификацией TCP/IP стека, позволяющей работать с несколькими экземплярами стека протоколов одновременно, а также портировал открытый (open source) код для сетевых приложений на VxWorks. Он работал в области встраиваемых систем последние восемь лет одновременно в роли интегратора конечных продуктов и конструктора законченных решений в виде встраиваемых операционных систем. Его работа была сфокусирована на сетевых аспектах встраиваемых систем, но он также занимался общими вопросами по широкому кругу аспектов данных систем. Невил-Нейл в настоящее время работает над новым коммерческим DHCP сервером в компании Nominum. Он также проводит семинары и уроки.

Джордж Невилл-Нейл: Как вы оказались вовлечены в работу с НАСА над их космическими проектами?

Майк Делиман: В 1994 году компанию Wind River Systems попросили портировать ее операционную систему на процессоры, защищенные от радиационного излучения, на основе чипа IBM Power, 32-битного предшественника теперешней линейки PowerPC. Чип Power также назывался RS6000; версия с защитой от радиации называлась Rad6000. Мне повезло, что меня попросили помочь с доводкой программного обеспечения Wind River и стать экспертом как по процессору, так и по портированию VxWorks. Все, кто также работал над этим, впоследствии занялись другими вещами, но я остался помогать сотрудникам НАСА использовать наше ПО в других задачах, связанных с космосом: Deep Space 1, SeaWinds, SMEX (Small Explorer Project), Genesis, Stardust, SORCE (Solar Radiation and Climate Experiment), Gravity Probe B и в ряде других зондов и спутников.

Когда начался проект MER (Mars Exploration Rover) меня вызвали и спросили, есть ли человек, который покинул проект Pathfinder и может работать над MER. Таким человеком оказался я.

Нейл: Что означает "защита от радиации" для процессора?

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

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

Нейл: Какова ваша роль в работе с НАСА?

Делиман: В проекте MER с 2001 года по февраль 2004 я был главным инженером операционной системы. Я делал расширения, модификации, исправления ошибок, проводил исследования и портирование — практически все, что касалось участия Wind River в данном проекте.

После этого я покинул Wind River и сейчас работаю в лаборатории реактивных двигателей НАСА.

Нейл: Не могли бы Вы рассказать нам немного о работе в Wind River? Сколько еще людей из Wind River работали над ПО для НАСА и как строились отношения между этими организациями?

Делиман: Я единственный из Wind River, кто работал над ПО для Rad6000. Я консультировался с другими инженерами по специфическим вопросам, но я был единственным инженером, отвечающим за поддержку процессора Rad6000 со стороны Wind River.

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

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

Следующая глава: "Написание кода для космических аппаратов".

Оригинал "And you think your operating system needs to be reliable."

Реклама
Рубрика: космос | Оставить комментарий

FPO

Перевод с блога Larry Osterman’s Weblog. Оригинал здесь.

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

Причина, по которой я был удивлен заключалась в том, что я не представлял себе что кто-нибудь до сих пор использует FPO.

Что же такое FPO?

Чтобы ответить на этот вопрос, надо вспомнить предысторию.

Процессор Intel 8088 обладал чрезвычайно ограниченным набором регистров (я не учитываю сегментные регистры), это были:

 AX  [BX]  CX  DX IP
[SI] [DI] [BP] SP FLAGS

При таком ограниченном наборе регистров, у каждого регистра было свое предназначение. AX, BX, CX, и DX были регистами "Общего назначения", SI и DI — "Индексные" регистры, SP — "Stack Pointer" (указатель на стек), BP — "Frame Pointer" (указатель на фрейм стека), IP — "Instruction Pointer" (указатель инструкций) и FLAGS — регистр, доступный только для чтения, который содержал несколько битов, отражающих текущее состояние процессора (к примеру, был ли результат последней арифметической или логической операции равен 0).

Регистры BX, SI, DI и BP были особенными, потому что они могли использоваться как "индексные" регистры. Индексные регистры чрезвычайно важны для компилятора, потому что они используются для доступа к памяти через указатель. Другими словамм, если имеется структура, которая размещается в памяти по смещению 0х1234, вы можете загрузить в индексный регистр значение 0х1234 и обращаться к полям этой структуры относительно этого адреса. Например, команды:

MOV BX, [Structure]
MOV AX, [BX]+4

загрузят в регистр BX значение, содержащееся в указателе Structure, а затем поместят в регистр AX значение слова, расположенного в по адресу Structure + 4.

Еще следует отметить, что регистр SP не являлся индексным регистром. Это означало, что для доступа к переменным, расположенным на стеке, необходимо было использовать другой регистр — вот откуда появился регистр BP — он был предназначен для доступа к значениям из стека.

Когдя появился 386-й процессор, в нем регистры были расширены до 32-х бит и все 8 регистров могли использоваться в качестве индексных.

[EAX] [EBX] [ECX] [EDX] EIP
[ESI] [EDI] [EBP] [ESP] FLAGS

Это было здорово, теперь вместо 4-х регистров в качестве индекстных, компилятор мог использовать 8.

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

Особо проницательный читатель может сообразить, что, поскольку ESP теперь также является индексным регистром, то можно больше не резервировать EBP для доступа к переменным на стеке. Другими словами, вместо:

MyFunction:
  PUSH    EBP
  MOV     EBP, ESP
  SUB      ESP, <LocalVariableStorage>
  MOV     EAX, [EBP+8]
  :
  :
  MOV     ESP, EBP
  POP      EBP
  RETD

для доступа к первому параметру на стеке (EBP+0 — это старое значение EBP, EBP+4 — это адрес возврата) можно сделать следующее:

MyFunction:
  SUB     SP, <LocalVariableStorage>
  MOV     EAX, [ESP+4+<LocalVariableStorage>]
  :
  :
  ADD     SP, <LocalVariableStorage>
  RETD

Это работает замечательно — в одночасье стало возможно использовать регистр EBP как и остальные регистры общего назначения! Парни из команды компилятора назвали эту оптимизацию "Исключение указателя на фрейм" ("Frame Pointer Omission"), а нам она известна по аббревиатуре FPO.

Но есть одна маленькая проблема, связанная с FPO.

Если посмотреть на вариант функции MyFunction без FPO, то можно увидеть, что первые две инструкции — это PUSH EBP; MOV EBP, ESP. Они обладали интересным и чрезвычайно полезным сторонним эффектом. Фактически, они создавали односвязный список, который связывал фреймы всей цепочки вызова вплоть до данной функции. При помощи EBP можно восстановить весь стек вызовов. Это было невероятно полезно для отладчиков — можно было восстанавливать стеки вызовов даже если не было отладночной информации для всех отлаживаемых модулей. К сожалению, при использовании FPO, этот список теряется — информация просто нигде не сохраняется.

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

FPO использовалась для всех модулей в Windows в версии NT 3.51, но была отключена в Vista, поскольку в ней не было необходимости — компьютеры стали настолько быстрее по сравнению с 1995 годом, что то небольшое увеличение производительности, которое давало FPO, не стоило тех проблем, которые она создавала при отладке и анализе.

Рубрика: оптимизация | 1 комментарий

Опасайтесь неявных преобразований в С++

Перевод с блога The Old New Thing. Оригинал здесь.

Темой для сегодняшний разговор послужил вопрос клиента:

"Я пытаюсь устранить ошибку, связанную с переполненим стека. Чтобы уменьшить размер стекового фрейма я удалил все локальные переменные, какие только мог, но все равно фрейм остается слишком большим и я не могу понять, где выделяется эта память. Что еще находится в стеке кроме локальных переменных, параметров, сохраненных регистров, и адреса возврата? Да, есть еще данные для структурной обработки исключений (SEH), но они обычно не занимают так много памяти и поэтому не могут быть причиной такого таинственного потребления стека…"

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

class BigBuffer
{
public:
    BigBuffer(int initialValue)
    { memset(buffer, initialValue, sizeof(buffer)); }
private:
    char buffer[65536];
};

extern void Foo(const BigBuffer& o);

void oops()
{
    Foo(3);
}

"Разве это вообще скомпилируется? Функция Foo ожидает BigBuffer, а не int!" Да, это скомпилируется.

Это произойдет потому что компилятор использует клнструктор класса BigBuffer как конвертер. Другими словами, компилятор добавлет вот такую временную переменную:

void oops()
{
    BigBuffer temp(3);
    Foo(temp);
}

Он это делает, потому что конструктор, который принимает ровно один параметр может использоваться двояко:
как обычный конструктор (что мы видим в случае с BigBuffer temp(3) ) или для того, чтобы обеспечить
неявное преобразование типа аргумента в конструируемый тип. В этом случае конструктор BigBuffer(int) используется для преобразования типа int в тип BigBuffer.

Чтобы этого избежать следует использовать ключевое слово explicit.

class BigBuffer
{
public:
    explicit BigBuffer(int initialValue)
    { memset(buffer, initialValue, sizeof(buffer)); }
private:
    char buffer[65536];
};

Теперь, вызов Foo(3) приведет к ошибке компиляции:

sample.cpp: error C2664: ‘Foo’ : cannot convert parameter 1 from
‘int’ to ‘const BigBuffer &’
Reason: cannot convert from ‘int’ to ‘const BigBuffer’
Constructor for class ‘BigBuffer’ is declared ‘explicit’

Рубрика: Uncategorized | Оставить комментарий

Почему гранулярность адресного пространства составляет 64К?

Перевод с блога The Old New Thing. Оригинал здесь.

Возможно, вам любопытно, почему VirtualAlloc выделяет память по границам 64К, хотя страничная гранулярность равна 4К.

За это следует благодарить процессор Alpha AXP.

На этом процессоре не существует команды "загрузить 32-битное целое значение". Для того, что сделать это, необходимо загрузить два 16-битный целых значения и объединить их.

Таким образом, если бы гранулярность была точнее, чем 64К, то DLL библиотека, загруженная не по предполагаемому адресу, потребовала бы двух операций при подстройке каждого адреса: одну для страших 16 бит и одну для младших.

Все обстоит гораздо хуже, если в результате этого происходит перенос или заем между двумя половинами. (Например, для изменения адреса на 4К с 0х1234F000 на 0х12350000 требуется изменить и старшую и младшую части адреса. Даже притом, что величина изменения намного меньше 64К, оно тем не менее влияет и на старшую часть из-за переноса.)

Но и это еще не все.

Alpha AXP на самом деле складывает два знаковых 16-битных целых значения, чтобы получить 32-битное целое. Так, для того чтобы загрузить значение 0х1234ABCD, вы должны вначале использовать команду LDAH, чтобы загрузить значение 0х1235 в старшее слово нужного регистра. А затем при помощи команды LDA необходимо к нему прибавить знаковое значение -0x5433. (Поскольку 0х5433 = 0х10000 — 0хABCD.) Результатом будет требуемое значение 0х1234ABCD.

LDAH t1, 0x1235(ноль) // t1 = 0x12350000
LDA t1, -0x5433(t1)       // t1 = t1 — 0x5433 = 0x1234ABCD

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

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

Гранулярность памяти в 64К устраняет все эти проблемы.

Если Вы были очень внимательны, то Вы должны были заметить, что это также дает ответ на вопрос, почему существуют "запретные земли" около границ 2 гигабайтов. Рассмотрим, как формируется значение 0x7FFFABCD: Так как младшие 16 бит находятся в верхней половине 64-килобайтного диапазона, то итоговое значение должно быть сформировано при помощи вычитания, а не сложения. Очевидное решение такое

LDAH t1, 0x8000(ноль)  // t1 = 0x80000000, так?
LDA t1, -0x5433(t1)        // t1 = t1 — 0x5433 = 0x7FFFABCD, верно?

Верно, за тем исключением, что это не работает. Alpha AXP — это 64 битный процессор, а 0х8000 не попадает в 16-битное знаковое целое, поэтому приходится использовать -0х8000, отрицательное число. Что на самом деле означает:

LDAH t1, -0x8000(ноль) // t1 = 0xFFFFFFFF`80000000
LDA t1, -0x5433(t1)        // t1 = t1 — 0x5433 = 0xFFFFFFFF`7FFFABCD

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

ADDL t1, zero, t1           // t1 = t1 + 0, с суффиксом L
                                      // Суффикс L означает знаковое расширение результата с 32 бит до 64
                                      // t1 = 0x00000000`7FFFABCD

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

Это была бы чрезвычайно высокая цена за доступ к последним 64К памяти адресного пространства (50% снижение производительности для всех вычислений адресов, чтобы защититься от случая, который практически не случается), таким образом, обозначение этой области как недоступной является вполне разумным решением.

Рубрика: память | Оставить комментарий

Существует ли в Windows ограничение в 2000 потоков на каждый процесс на самом деле?

Перевод с блога The Old New Thing. Оригинал здесь.

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

Поток состоит из некоторого объема памяти в режиме ядра (стек ядра и управление объектами), некоторой памяти в режиме пользователя (блок переменных окружения, локальная память потока, к примеру), плюс стек потока. (Или стеки, если вы работаете на Itanium системе.)

Как правило, ограничиваюим фактором является именно размер стека.

#include <stdio.h>
#include <windows.h>

DWORD CALLBACK ThreadProc(void*)
{
      Sleep(INFINITE);
      return 0;
}

int __cdecl main(int argc, const char* argv[])
{
      int i;
      for (i = 0; i < 100000; i++) {
            DWORD id;
            HANDLE h = CreateThread(NULL, 0, ThreadProc, NULL, 0, &id);
            if (!h) break;
            CloseHandle(h);
      }
      printf("Created %d threads\n", i);
      return 0;
}

Эта программа скорее всего напечатает значение около 2000 созданных потоков. Почему именно 2000?
Потому что по умолчанию размер стека, задаваемый компоновщиком равен 1Мб, и 2000 стеков по 1Мб равны примерно 2Гб, то есть тому объему, который доступен для программ в режиме пользователя.

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

HANDLE h = CreateThread(NULL, 4096, ThreadProc, NULL, STACK_SIZE_PARAM_IS_A_RESERVATION, &id);

После такой модификации, мне удалось создать около 13.000 потоков. Несмотря на то, что это однозначно лучше, чем 2000, было бы наивно ожидать создания 500.000 потоков. (Поток использует 4Кб для стека в 2Гб адресном пространстве.) Но вы забываете о другом ограничении. Гранулярность адресного пространства составляет 64Кб, так что каждый стек занимает 64Кб адресного пространства даже тогда, когда только 4Кб из них используются. Ну и конечно, у вас нет полноценных 2Гб адресного пространства: системные DLL и прочие вещи также располагаются здесь.

Но главный вопрос, который возникает, когда кто-то спрашивает: "Чему равно максимальное число потоков, которое может создать процесс?" таков: "А почему вы создаете так много потоков, что это становится актуально?"

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

Обратите внимание, что "волокна" (fibers) здесь особо не помогут, потому что они имеют стеки, а почти всегда именно адресное пространство, необходимое для стека, является ограничивающим фактором.

Рубрика: Uncategorized | Оставить комментарий

Оптимизация зачастую противоречит здравому смыслу.

Перевод с блога The Old New Thing. Оригинал здесь.

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

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

__declspec(noinline)
void *GetCurrentAddress()
{
      return _ReturnAddress();
}


void *currentInstruction = GetCurrentAddress();

Если взглянуть на дизассемблированный код, то можно увидеть что-то вроде:

GetCurrentAddress:
      mov eax, [esp]
      ret 

      …
      call GetCurrentAddress
      mov [currentInstruction], eax

"Е-мое" — скажете вы про себя — "вы только посмотрите, насколько это неэффективно. Я могу реализовать это
всего двумя инструкциями. Смотрите:

void *currentInstruction;
__asm {
      call L1
L1: pop currentInstruction
}

В два раза меньше, чем в этой раздутой версии."

Однако если сесть и сравнить скорость работы этих двух вариантов, то вы обнаружите, что вариант с вызовом фукнции работает в два раза быстрее! Как такое может быть?

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

Последние модели Pentium (и я верю, что Athlon также) процессоров поддерживают внутренний стек, который
обновляется при каждой CALL и RET инструкции. Когда выполняется команда CALL, адрес возврата помещается в обычный стек (тот, на который указывает регистр ESP), а также во внутренний стек предсказания адресов возврата; команда RET извлекает адрес с вершины стека предсказания адресов и с обычного стека.

Стек предсказания адресов возврата используется в тот момент, когда процессор декодирует команду RET. Он смотрит на вершину этого стека и говорит: "Готов поспорить, что команда RET собирается вернуть управление вот на этот адрес." А затем, на основании этой информации, процессор начинает выполнять инструкции по этому адресу. Поскольку программы редко химичат с адресами возврата на стеке, то такое предсказание в подавляющем большинстве случаев оказывается верным.

Вот почему, та "оптимизация" привела к замедлению работы. Допустим, в точке команды CALL L1, стек предсказания возвратов выглядел так:

стек предсказаний: caller1 -> caller2 -> caller3 -> …
основной стек: caller1 -> caller2 -> caller3 -> …

Здесь, caller1 — это функция, вызвавшая текущую функцию, caller2 — функция, вызвавшая caller1 и так далее. Сейчас стек предсказаний отражает фактическую ситуацию. (Ниже для сравнения приведен основной стек и вы видите, что они совпадают.)

Теперь мы выполняем инструкцию CALL. После этого стеки будут выглядеть так:

стек предсказаний: L1 -> caller1 -> caller2 -> caller3 -> …
основной стек: L1 -> caller1 -> caller2 -> caller3 -> …

Но вместо выполнения инструкции RET, вы извлекаете адрес возврата из основного стека. Это удаляет его из основного стека, но не удаляет из стека предсказания возвратов.

стек предсказания возвратов: L1 -> caller1 -> caller2 -> caller3 -> …
основной стек: caller1 -> caller2 -> caller3 -> caller4 -> …

Я думаю, понятно к чему это приводит.

В конце концов, ваша функция завершается. Процессор декодирует вашу инструкцию RET и смотрит в стек предсказаний, и говорит: "Мой стек предсказаний говорит, что эта команда RET собирается передать управление функции L1. Я начинаю выполнять инструкции по тому адресу."

Но нет, значение на вершине основного стека не вовсе не L1. А caller1. Процессорный предсказатель предсказал неверно, и в результате время было потрачено на изучение неверного кода!

Эффекты от этой неверной догадки на этом не заканчитваются. После инструкции RET, стек предсказаний выгялдит так:

стек предсказаний: caller1 -> caller2 -> caller3 -> …
основной стек: caller2 -> caller3 -> caller4 -> …

Когда-нибудь завершится и caller1. И снова, процессор на основании данный из стека предсказаний начнет выполнять код функции caller1. Но теперь мы возвращается не в caller1, а в caller2.

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

Эта оптимизация оказалась близорукой.

Некоторые процессоры приокрывают свой предсказатель. Alpha AXP, к примеру, имеет несколько типов инструкций передачи управления, которые имеют один и тот же логический эффект, но по-разному работающие с внутренним стеком предсказаний процесора. Например, инструкция BR говорит: "Перейди к этому адресу, но не помещай старый адрес в стек предсказаний". С другой стороны, команда JSR говорит: "Перейди к этому адресу и помести старый адрес в стек предсказаний". Существует, также инструкция RET, говорящая: "Перейди к этому адресу и извлеки адрес из стека предсказаний" (Еще есть и четвертый тип, который редко используется.)

Мораль басни: Если что-то выглядит лучше, еще не означает, что оно лучше на самом деле.

Рубрика: оптимизация | 1 комментарий

Охота за скоростным обработчиком системного прерывания.

Перевод с блога The Old New Thing. Оригинал здесь.

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

Я вспомнил совещание между сотрудниками Интел и Микрософт, которое имело место более 15 лет назад. (К сожалению, что меня там не было, так что эта история не из первых рук.)

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

На этом собрании представитель Интел спросил: "Если бы вы могли попросить ускорить только одну вещь, о чем бы вы попросили?"

Один из ведущих разработчиков ядра без колебаний ответил: "Ускорьте обработку исключительной ситуации, возникающую при недопустимой инструкции"

Интеловская половина комнаты засмеялась. "Ну вы тут в Микрософте и приколисты!" Таким образом, собрание завершилось веселой шуткой.

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

Нет, не шутил.

Так получилось, что на процессоре 80386 в то время, наиболее быстрым способом переключиться из режима виртуального 86-го в режим ядра, являлось выполнение некорректной инструкции! Следовательно Windows/386 использовала некорректную инструкцию, в качестве ловушки системного прерывания.

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

Рубрика: оптимизация | Оставить комментарий