QNX RTP Logo QNX Realtime Platform: Русский Портал QNX
Thursday, 20 Nov 2008 10:57
Меню

Проект OpenNET - все о Unix
SMP: Два процессора и более Print E-mail
Mario Charest, перевод Олег Плессер

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

По сравнению с сетью, SMP предоставляет вам много преимуществ. Для начала любой процессор имеет доступ к той же самой памяти, PCI устройствам, портам ввода/вывода и другому железу. Но при этом требуется чтобы ваша операционная система поддерживала SMP - например такая поддержка есть в WinNT, Win2000, Linux, VxWorks и конечно же в QNX Neutrino. Win98 не поддерживает технологию SMP; даже если у вас установлено 16 процессоров в машине под управлением Win98, использовать будет только один процессор.

В наши дни использование технологии SMP становится очень выгодно. Я пишу эту статью на компьютере с двумя процессорами Celeron 500. Когда я покупал этот компьютер, она стоила дешевле чем компьютер на базе Pentium-III 600. К сожалению последние версии Celeron'а уже нельзя использовать в SMP системах. Но через несколько месяцев в продаже появится системные платы с поддержкой двух процессоров типа Athlon. Если посмотреть на прайс-лист процессоров, то можете сравнить два Duron 750 и один Pentium-III 750 и увидеть разницу в 50$ сэкономленные для покупки более дорогой системной платы :-).

SMP технология позволяет вам делать куда больше. Главное учитывать, что программа(процесс) должна уметь использовать возможности SMP технологии. Например игра Unreal Tournament не будет работать быстрее на компьютере с несколькими процессорами чем на компьютере с одним процессором. Но в тоже время когда вы играете в Unreal Tournament Вы можете записывать CD или загружать данные из Интернет не уменьшая скорость самой игры. Под Windows когда я печатаю документ на машине с одним процессором, система начинает работать медленнее, так как она занимается пересылкой данных на параллельный порт. Но компьютер с поддержкой SMP, работает значительно быстрей. Так как один процессор занят передачей данных на параллельный порт, а второй может выполнить любую другую задачу.

Если у вас есть компьютер с двумя процессорами Celeron 500, и вы используете ОС QNX, то при компиляции программы с ключом -j2 Вы получите прирост скорости до 60%. Хотя использование процессора Pentium-III с тактовой частой 1 GHz дало бы более лучший результат, вы заплатили бы за него в три раза больше.

Теперь я бы вам рекомендовал вам перейти к документации QSSL в раздел SMP поддержки (http://qdn.qnx.com/support/docs/neutrino_qrp/sys_arch/smp.html) прежде чем продолжить. Ее прочтение позволит вам разобраться в особенностях использования SMP и поможет лучше понять последующий материал.

Разделение памяти.

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

Следующий пример показывает эффект разделения памяти.

#include <stdio.h> #include <time.h> #include <stdlib.h> #include <string.h> #define LOOP 50 #define SIZE 50000000 int main(int argc, char* argv[]) { int t; int i; float elapse; static char dummy[SIZE]; t = clock(); /* Функция clock() возвращает примерное число тактов сделанным процессоров с начала включения*/ for(i=0; i<LOOP; i++ ) { memset( dummy, 0, sizeof( dummy) ); /*функция memset записывает в массив dummy значение 0 */ } elapse = ( clock()-t ) / (float)CLOCKS_PER_SEC; /* CLOCKS_PER_SEC переменная определенная в файле time.h и показывающая скорость работы процессора (число тактов в секунду) */ printf(Duration %f sec, %.1f MBytes/sec \n, elapse, sizeof(dummy)*LOOP/elapse/1000000); return 0; } >

Эта программа заполняет 50 мегабайт памяти нулями 50 раз подряд. Когда эта программа запускается в одном экземпляре, то ее время потраченное на ее работу равняется примерно 14.3 секунды, со скоростью передачи данных 175 Mб/сек. Если я запускаю эту программу в двух экземплярах, то время выполнения каждой становится равным примерно 24 секунды, а скорость передачи данных каждой программы составляет 100 Мб/сек. В итоге общая скорость работы с памятью становится равным 200 Мб/сек, что является 12.5% приростом по скорости (как мы видим это незначительный прирост)

Эта программа демонстрирует крайний случай, когда вся информация не может быть записана в кэш процессора. Также обратите внимание, что моя машина Celeron. Он имеет маленький кэш и работает на медленной шине по сравнению с процессорами Athlon или Pentium. Соответственно использование SMP системы здесь дает небольшой выигрыш. Выполняя эту программу последовательно два раза заняло бы по времени около 28 секунд. Но запуск в параллель дал нам 24 секунды.

Если мы в нашей программе сократим размер заполняемой памяти до 50 килобайт, то нам тогда должно хватить кэша нашего процессора. Так как у программы не будет проблемы разделения памяти. Увеличим число циклов до 250000 раз, что бы иметь такой же объем передаваемых данных. Выполнение единственного экземпляра программы займет теперь 10.6 секунды, при скорости передачи данных 1180 Мб/сек. Когда же мы одновременно запустим две программы мы получим примерно такое же время и скорость передачи данных для обоих программ. Что даст нам общую скорость 2360 Мб/сек. Как вы можете видеть вашим программам надо намного меньше обращаться к общей памяти, таким образом избегая простоя. Фактически мы получили 100% прирост производительности нашей системы.

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

Кэш процессора

Кэш процессора один из факторов объясняющих почему современные процессоры так быстро работают. Без использования кэша современным процессорам (например с тактовой частотой 1 гигагерц и частой работы памяти 100 мегагерц) пришлось бы простаивать ожидая когда они получат данные. В КЭШе должны находиться наиболее часто используемые данные и данные которые недавно обрабатывались. Попробуйте в БИОСе отключить использование КЭШа и вы увидите насколько упадет производительность вашей системы.

Когда мы используем SMP, ядро ОС должно разумно использовать кэш процессора. Любой поток в зависимости от готовности процессора может быть перемещен с одного процессора на другой. Однако такая схема имеет свои недостатки. Например, если поток хранит свои данные в КЭШе первого процессора, то после переброса потока на другой процессор, в КЭШе первого процессора они должны быть перенесены в общую память, откуда их загрузит в свой кэш второй процессор. При перемещении потока, желательно избегать эффектов перегрузки данных из одного КЭШа в другой. Для достижения этой цели в SMP ядре Neutrino используется эвристический подход для оценки эффективности использования переноса потоков.

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

Вот наверное наиболее частый пример:

char array[256000]; memset( array, 0, sizeof( array ) ); for ( cnt = 0; cnt < sizeof( array ); cnt++ ) do_something( array[cnt] );

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

А вот решение:

char array[256000]; memset( array, 0, sizeof( array ) ); for ( cnt = sizeof( array )-1; cnt >=0; cnt--) do_something( array[cnt] );

Итак первый пример. Понятно что массив размером 256000 байт не поместиться в кэш Celeron. Когда функция memset заканчивает свою работу, начало массива уже находится вне кэша процессора. Что получаем мы: мы копируем снова начало массива в кэш, на чем теряем драгоценное время. Я привел пример с массивом размера 256 килобайт, что бы вам было проще понять эту идею. В операционной системе подобной Neutrino у вас работают помимо вашей программы другие программы (например программа обработчика прерываний). Даже если во время их работы ваши данные будут выгружаться из кэша, второй вариант решения более предпочтителен.

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

Другое решение этой проблемы состоит в уменьшение обрабатываемых блоков. Обработка небольших блоков проходит быстрее чем одна большая. Давайте посмотрим это на следующем примере:

#include "stdafx.h" #include <stdio.h> #include <time.h> #include <stdlib.h> #include <string.h> #define BLOCK_SIZE 10000 void work1( char *data, size_t size ) { size_t cnt; for( cnt = 0 ; cnt < size; cnt++, data++ ) { *data ^= 1; } } void work2( char *data, size_t size ) { size_t cnt; for( cnt = 0 ; cnt < size; cnt++, data++ ) { *data |= 2; } } void work3( char *data, size_t size ) { size_t cnt; for( cnt = 0 ; cnt < size; cnt++, data++ ) { *data += 3; } } int main(int argc, char* argv[]) { int t; int i; float elapse; static char dummy[BLOCK_SIZE * 10000]; t = clock(); // --- один большой блок memset( dummy, 0, sizeof( dummy ) ); work1( dummy, sizeof( dummy ) ); work2 ( dummy,sizeof( dummy ) ); work3 ( dummy,sizeof ( dummy ) ); elapse = ( clock()-t ) / (float)CLOCKS_PER_SEC; printf("Duration %f sec(s)\n", elapse ); t = clock(); // --- маленькие блоки for( i=0; i<sizeof( dummy ) ; i+= BLOCK_SIZE ) { memset( &dummy[i], 0, BLOCK_SIZE ); work1 ( &dummy[i], BLOCK_SIZE ); work2 ( &dummy[i], BLOCK_SIZE); work3 ( &dummy[i], BLOCK_SIZE); } elapse = ( clock()-t ) / (float)CLOCKS_PER_SEC; printf(Duration %f sec(s)\n , elapse ); return 0; } >

На Celeron мы получим следующий результат:

    Duration 8.609000 sec(s)
    Duration 6.766000 sec(s)

Даже с учетом накладных расходов на цикл, где мы вызываем 4 функции мы получили прирост более чем на 20%. Убедились? Я мог бы еще больше повысить производительность данной программы если бы в функциях work_1 и work_3 обрабатывал данные в цикле от большего к меньшему.

Также стоит отметить вниманием сравнение обычной системы относительно SMP системы, если вы сравниваете системный блок с двумя Celeron 500MHz c системным блоком с одним Celeron 1000 MGz. SMP система имеет в нашем примере вдвое больше кэша, что дает основание отдавать предпочтение SMP системе. Чем кэша больше тем лучше

FIFO планирование (планирование в порядке поступления)

На SMP системах, потоки действительно работают параллельно. Зачастую под многозадачной системой понимают систему которая одновременно обрабатывает несколько задач, но это не так. Процессор поочередно выполняет различные программы. Достаточно очевидный пример FIFO планирования QNX4. Если запущено одновременно несколько программ с одним приоритетом в режиме планирования FIFO, то когда одна из этих программ получает процессор в свое распоряжение, другие программы не будут выполняться до тех пор, пока первая не освободит процессор. Я видел некоторые проекты использующих такую систему планирования постоянно, и сам использовал такую систему несколько раз. При использования такой системы планирования можно добиться защиты общих данных без дополнительных накладных расходов. Но на системе с использованием SMP такой подход неприменим, это просто не будет работать. Режим планирования FIFO не работает на системах SMP.

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

Приоритеты (Планирование по приоритетам)

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

Процессор Xeon

Я работал над проектом, где в качестве системы использовалась система с четырьмя процессороми Xeon (данный процессор имеет кэш размером 1 Mb). Полученным опытом я могу поделиться с вами.

Прежде чем остановить свой выбор на этой системе я должен был ее протестировать на критические случаи, аналогично описанным выше в этой статьи. Запустив сначала одну программу, а потом одновременно две, я был удивлен результатами. В первом случае я получил результат равным 10 секундам, во втором 12. То есть при одновременном выполнении обоих программы я потерял всего лишь 20% времени. Очевидно в XEON, в отличие от Celeron, встроена поддержка SMP при работе с кэшем. Я был потрясен этими результатами.

Использование четырех процессорной системы.

Эта машина должна была использоваться для управления роботом с двумя руками, с семью сочленениями каждая. Вы наверное можете представить себе такую систему, и предположить насколько качественным должно быть ее управление. Чтобы можно было точно ею управлять процессоры должны были быть не меньше 1000 MGz. Компьютер должен был протоколировать свою работу на жесткий диск, работать с сетью (используя TCP/IP) с высокой скоростью и выполнять еще несколько задач.

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

Прерывания

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

Привязка к процессору

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

Тесты показали прирост производительности, если два потока были привязаны к второму и третьему процессорам. Оба потока выполнялись с приоритетом 63. Так как это самый высокий приоритет в системе, мы были уверены, что ни один другой поток не сможет повлиять на тестирование. В действительности, нам очень хотелось отдать процессор в полное распоряжение одного потока, т.е. реализовать что-то вроде эксклюзивного режима. Мы могли измерить количество "промахов" в стеке, появляющихся из-за других потоков, пока высокоприоритетные потоки были заблокированы в состоянии приема. Эти тесты проводились на очень ранней бета-версии Neutrino 2.1. Вполне возможно, что сейчас нам бы не пришлось делать привязку потоков к процессорам.

Побочные эффекты

Однажды при реализации проекта я столкнулся с мнимой ошибкой на SMP системе. Для меня это послужило хорошим уроком. Три дня я потратил на поиск ошибки который на самом деле не было.

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

Я запустил тестирующую программу, и поначалу все было хорошо. Я не ждал никаких проблем, ведь это всего лишь математика, не более того. Последним тестом я решил полностью заполнить круговой буфер. Для контроля работы своей программы, я с помощью функции printfs() выводил на экран индексы буфера. Таким образом я мог проверить насколько эффектно работает моя система. И только при полной нагрузки я заметил что данные в буфере индексируются как то странно. Я не мог заполнить полностью этот буфер. Я несколько раз проверял код, но не мог найти ошибки. Я попытался отвлечься от этой задачи, сделал другую задачу, надеясь что мои мозги проветрятся и я увижу свою ошибку. Когда я вернулся, то ошибка стала очевидна. Я тестировал свою программу на SMP системе. И оба потока выполнялись одновременно. Соответственно как бы я не старался поток отвечающий за запись данных на носитель будет считывать их из буфера быстрее, чем поток отвечающий за их запись туда.

Поллинг (циклический опрос)

Систему кругового опроса приложений не стоит применять в системах реального времени. Но иногда его использование оказывается крайне выгодным. Например, если процесс должен обработать что-нибудь 100000 раз в секунду, круговой опрос предпочтителен. Это будет в пределах возможностей системы. Вы можете генерировать 100 аппаратных прерываний, но это будет тяжелой задачей для процессора. Даже для современных высокоскоростных процессоров 100000 прерываний в секунду это чересчур. А вот на SMP системе это проблемой не будет. Можно написать программу циклического опроса таймера для точной проверки истекшего времени и выполнять ее каждые 10 мс. Но, конечно же, для каждой такой программы необходим "свой" свободный процессор.

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

Заключение

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

[Вернуться к списку]
©   2000-2003 Команда проекта QNX.ORG.RU // QNX.ORG.RU Team
Авторы проекта: Дмитрий Алексеев [dmi] и Дмитрий Васильев. Техническое сопровождение проекта: Игорь Сорокин [isorokin]. Информационное сопровождение: Дмитрий Алексеев [dmi]
QNX - зарегистрированная торговая марка QNX Software Systems, Ltd., Canada. Остальные упоминаемые на сайте торговые марки и логотипы являются исключительно собственностью их уважаемых владельцев. Ничьи права не затронуты. Материалы сайта не могут быть скопированы и где-либо использованы в той или иной форме без письменного разрешения разработчиков сайта.
Powered by Mambo Open Source