![]() | ||||||||||||||||||||||||||||||
![]() |
![]() |
|
|
|||||||||||||||||||||||||||
![]() |
|
Разработка ресурсоемких приложений в среде Visual C++Источник: realcoding
|
Конфигурация (обратите внимание на RAID) | Время сборки среднего проекта, использующего большое количество внешних библиотек. |
AMD Athlon(tm) 64 X2 Dual Core Processor 3800+, 2 GB of RAM,2 x 250Gb HDD SATA - RAID 0 | 95 минут |
AMD Athlon(tm) 64 X2 Dual Core Processor 4000+, 4 GB of RAM,500 Gb HDD SATA (No RAID) | 140 минут |
Уважаемые руководители! Поверьте, что экономия на вычислительной технике с лихвой окупается простоями в работе программистов. Такие компании как Microsoft обеспечивают разработчиков последними моделями вычислительно техники, не от щедрости и расточительности. Они как раз хорошо умеют считать деньги и их пример не следует игнорировать.
На этом текст, посвященный руководителям, закончен, и мы вновь хотим обратиться к создателям программных решений. Требуйте, требуйте для себя той техники, которую считаете себе необходимой. Не стесняйтесь, в конце концов Ваш начальник, скорее всего, может просто не понимать, что это выгодно всем. Нужно заниматься просветительской работой. Тем более в случае отставания в планах, виновным будете казаться Вы. Проще выбить новую технику, чем пытаться объяснить, на что Вы тратите время. Сами представьте, как может звучать Ваше оправдание о правке одной единственной ошибки в течение всего дня: "Так ведь проект большой прислали. Я запустил под отладчиком, долго ждал. А памяти у меня только 1 гигабайт. А больше ничем параллельно заниматься невозможно. Windows в своп ушел. Нашел ошибку поправил, но так ведь опять снова запустить и проверить нужно....". Ваш начальник возможно промолчит, но будет считать Вас просто лентяем. Не доводите до этого.
Ваша первоочередная задача при разработке ресурсоемкого приложения - это не проектирование будущей системы и даже не изучение теории, а заблаговременное требование о закупке всего необходимого аппаратного и программного обеспечения. Только после того можно смело и эффективно приступать к созданию ресурсоемких программных решений. Невозможно писать и проверять параллельные программы, без многоядерных процессоров. И невозможно писать систему для обработки больших объемов данных без необходимого объема оперативной памяти.
Прежде чем перейти к следующей теме, хочется поделиться еще некоторыми мыслями, которые помогут сделать работу более комфортной.
Проблему медленной сборки проекта можно попробовать решить путем использования специальных средств параллельной сборки подобной, например системе IncrediBuild by Xoreax Software (http://www.xoreax.com). Естественно существуют и другие подобные системы, которые можно поискать в сети.
Проблему тестирования приложений на огромных массивах данных (запуск пакетов с тестами), для которых рабочие машины недостаточно производительны, можно решить использованием нескольких специальных мощных машин с удаленным доступом. Примером удаленного доступа может служить Remote Desktop или X-Win. Обычно одновременно тестовые запуски осуществляет только малое количество разработчиков. И для коллектива из 5-7 человек вполне может хватить 2-х мощных выделенных машин. Это будет не самое удобное решение, но весьма экономичное, по сравнению с приобретением таких рабочих станций каждому разработчику.
Следующим препятствием, которое станет на Вашем пути, в разработке систем для обработки большого объема данных, будет то, что Вам, скорее всего, придется пересмотреть свою методологию работы с отладчиком или даже полностью отказаться от его использования.
Ряд специалистов предлагает отказаться от этой методологии тестирования по идеологическим соображениям. Основной аргумент состоит в том, что отладчик провоцирует использование метода проб и ошибок. Человек видя некорректное поведение алгоритма, на каком то, из этапов его выполнения тут же производит правки, не вникая в суть, почему эта ошибка была допущена и не задумывается над способом ее исправления. Если он не угадал с исправлением, то при следующем выполнении кода, он это заметит и внесет новые правки. Результатом становится менее качественный код. Причем автор этого кода далеко не всегда уверен, что понимает, как он работает. Противники отладки предлагают заменять ее более строгой дисциплиной разработки алгоритмов, использованием как можно более мелких функций, чтобы принципы их работы были очевидны. Также они предлагают уделять большее внимание юнит-тестированию и использовать системы логирования для анализа корректности работы программы.
В описанной критике систем отладки есть рациональные зерна, но, как и во многих других случаях следует все взвесить и не впадать в крайности. Использование отладчика часто удобно и может сэкономить много сил и времени.
Плохая применимость отладчиков при работе с системами, обрабатывающими большие объемы данных, связана, к сожалению, не с идеологическими, а практическими сложностями. Хочется познакомить читателей с этими сложностями, чтобы сэкономить их время на борьбу с инструментом-отладчиком, когда он уже малопригоден и подвигнуть их к поиску альтернативных решений.
Рассмотрим причины, требующие использования альтернативных средств вместо классического отладчика (например, встроенного в среду Visual C++).
1) Медленное выполнение программы.
Выполнение программ под отладчиком, обрабатывающей миллионы или миллиарды элементов может стать практически неосуществимым из-за временных затрат. Во-первых, необходимо использовать отладочный вариант кода с выключенной оптимизацией, что уже существенно замедляет скорость работы алгоритма. Во-вторых, в отладочном варианте происходит выделение большего объема памяти для контроля выхода за пределы массивов, заполнение памяти при выделении/удалении и так далее, что еще более замедляет время работы программы.
Можно резонно заметить, что отлаживать программу вовсе не обязательно на больших рабочих объемах данных, а обойтись тестовыми задачами. К сожалению, это не так. Неприятный сюрприз заключается в том, что при разработке 64-битных систем, Вы не можете быть уверены в корректности работы алгоритмов, тестируя их на небольших объемах данных, а не на рабочих объемах размером в гигабайты.
Приведем один простой пример, демонстрирующий проблему необходимости тестирования на большом объеме данных.
#include <vector> #include <boost/filesystem/operations.hpp> #include <fstream> #include <iostream> int main(int argc, char* argv[]) { std::ifstream file; file.open(argv[1], std::ifstream::binary); if (!file) return 1; boost::filesystem::path fullPath(argv[1], boost::filesystem::native); boost::uintmax_t fileSize = boost::filesystem::file_size(fullPath); std::vector<unsigned char> buffer; for (int i = 0; i != fileSize; ++i) { unsigned char c; file >> c; if (c >= 'A' && c <= 'Z') buffer.push_back(c); } std::cout << "Array size=" << buffer.size() << std::endl; return 0; } |
Данная программа читает файл и сохраняет в массиве все символы, относящиеся к заглавным английским буквам. Если все символы в выходном файле будут заглавными английскими буквами, то на 32-битной системе мы не сможем поместить в массив более 2*1024*1024*1024 символов, а следовательно и обработать файл более 2 гигабайт. Представим, что такая программа корректно использовалась на 32-битной системе, с учетом этого ограничения и никаких ошибок не возникало.
На 64-битной системе возникнет желание обрабатывать файлы большего размера, так как снимается ограничение на размер массива в 2 гигабайта. К сожалению, программа написана некорректно с точки зрения модели данных LLP64 (см. таблицу N1), используемой в 64-битной операционной системе Windows. В цикле используется переменная типа int, размер которой по-прежнему составляет 32 бита. В случае если размер файла будет равен 6 гигабайт, то условие "i != fileSize" никогда не будет выполнено и возникнет вечный цикл.
Данный код приведен, чтобы продемонстрировать сложность поиска с помощью отладчика ошибок, которые возникают только на большом объеме памяти. Получив зацикливание при обработке файла в 64-битной системе можно взять для обработки файл размером в 50 байт и просмотреть работу функции под отладчиком. Но ошибка на таком объеме данных не возникнет, а смотреть в отладчике обработку 6 миллиардов элементов невозможно.
Естественно следует понимать, что это всего лишь пример, и что его легко можно отладить и понять причину зацикливания. В сложных системах, к сожалению, такое часто становится практически нереализуемым из-за медленности обработки большого объема данных.
Более подробно с подобными неприятными примерами Вы можете познакомиться в статье "Забытые проблемы разработки 64-битных программ" и " 20 ловушек переноса Си++ - кода на 64-битную платформу".
2) Многопоточность.
Использование нескольких параллельно выполняемых потоков команд для ускорения обработки большого объема данных давно и успешно используется в кластерных системах и высокопроизводительных серверах. Но только с приходом на массовый рынок многоядерных микропроцессоров, возможность параллельной обработки данных начинает широко использоваться прикладным программным обеспечением. И актуальность разработки параллельных систем со временем будет только расти.
К сожалению, не просто объяснить, в чем состоит сложность отладки параллельных программ. Только столкнувшись с задачей поиска и исправления ошибок в параллельных системах можно почувствовать и понять беспомощность инструмента под названием отладчик. Но в целом проблемы можно свести к невозможности воспроизведения многих ошибок и влиянию процесса отладки на последовательность работы параллельных алгоритмов.
Более подробно с вопросами отладки параллельных систем Вы можете познакомиться в следующих статьях: " Технология отладки программ для машин с массовым параллелизмом", "Multi-threaded Debugging Techniques", "Detecting Potential Deadlocks".
Перечисленные трудности решаются использованием специализированных методологий и инструментов. С некорректным 64-битным кодом можно бороться, используя статические анализаторы, работающие с исходным кодом программы и не требующим его запуска. Примером может служить статический анализатор Viva64 .
Для отладки параллельных систем следует обратить внимание в сторону таких инструментов как TotalView Debugger (TVD). TotalView это отладчик для языков Си, Си++ и фортран, который работает на Юникс-совместимых ОС и Mac OS X. Он позволяет контролировать нити исполения (потоки, thread), показывать данные одного или всех потоков, может синхронизировать нити через точки останова. Он поддерживает также параллельные программы, использующие MPI и OpenMP.
Другим интересными приложениями является средства анализа многопоточности Intel® Threading Analysis Tools.
Перечисленные и оставшиеся за кадром инструменты, безусловно, полезны и могут стать хорошим подспорьем при разработке высокопроизводительных приложений. Но не стоит забывать и о такой проверенной временем методологии, как использование систем логирования. Отладка методом логирования за несколько десятилетий ничуть не утратила актуальности и остается верным средством, о котором мы поговорим более подробно. Единственное изменение, которое накладывает время на системы логирования, это возросшие к ним требования. Попробуем перечислить свойства, которыми должна обладать современная система логирования, для высокопроизводительных систем:
Система логирования, отвечающая таким качествам позволяет универсально решать как задачу отладки параллельных алгоритмов, так и отлаживать алгоритмы обрабатывающие огромные массивы данных.
В статье не будет приведен конкретный код системы логирования. Такую систему трудно сделать универсальной, так как она сильно зависит от среды разработки, особенностей проекта, предпочтений разработчика и многого другого. Вместо этого будет рассмотрен ряд технических решений, которые помогут Вам создать удобную и эффективную систему логирования, если в том возникнет необходимость.
Самым простым способом осуществить логирование является использование функции аналогичной printf, как показано в примере:
int x = 5, y = 10; ... printf("Coordinate = (%d, %d)n", x, y); |
Естественным недостатком является то, что информация будет выводиться как в отладочном режиме, так и в конечном продукте. Поэтому, следует модернизировать код следующим образом:
#ifdef DEBUG_MODE #define WriteLog printf #else #define WriteLog(a) #endif WriteLog("Coordinate = (%d, %d)n", x, y); |
Это уже лучше. Причем обратите внимание, что мы используем для выбора реализации функции WriteLog не стандартный макрос _DEBUG, а собственный макрос DEBUG_MODE. Это позволяет включать отладочную информацию в Release-версии, что важно при отладке на большом объеме данных.
К сожалению, теперь при компиляции не отладочной версии в среде Visual C++ возникает предупреждение: "warning C4002: too many actual parameters for macro 'WriteLog'". Можно отключить это предупреждение, но это является плохим стилем. Можно переписать код, как показано ниже:
#ifdef DEBUG_MODE #define WriteLog(a) printf a #else #define WriteLog(a) #endif WriteLog(("Coordinate = (%d, %d)n", x, y)); |
Приведенный код не является элегантным, так как приходится использовать двойные пары скобок, что часто забывается. Поэтому внесем новое усовершенствование:
#ifdef DEBUG_MODE #define WriteLog printf #else inline int StubElepsisFunctionForLog(...) { return 0; } static class StubClassForLog { public: inline void operator =(size_t) {} private: inline StubClassForLog &operator =(const StubClassForLog &) { return *this; } } StubForLogObject; #define WriteLog StubForLogObject = sizeof StubElepsisFunctionForLog #endif WriteLog("Coordinate = (%d, %d)n", x, y); |
Этот код выглядит сложным, но он позволяет писать одинарные скобки. При выключенном DEBUG_MODE этот код превращается в ничто, и его можно смело использовать в критических участках кода.
Следующим усовершенствованием может стать добавление к функции логирования таких параметров как уровня детализации и типа выводимой информации. Уровень детализации можно задать как параметр, например:
enum E_LogVerbose { Main, Full }; #ifdef DEBUG_MODE void WriteLog(E_LogVerbose, const char *strFormat, ...) { ... } #else ... #endif WriteLog (Full, "Coordinate = (%d, %d)n", x, y); |
Этот способ удобен тем, что решение, отфильтровать или не отфильтровать маловажные сообщения можно принять уже после завершения работы программы, используя специальную утилиту. Недостаток такого метода в том, что всегда происходит вывод всей информации, как важной, так и второстепенной, что может снижать производительность. Поэтому можно создать несколько функций вида WriteLogMain, WriteLogFull и так далее, реализация которых будет зависеть от режима сборки программы.
Мы упоминали о том, что запись отладочной информации должна как можно меньше влиять на скорость работы алгоритма. Этого можно достичь, создав систему накопления сообщений, запись которых происходит в параллельно выполняемом потоке. Схематично этот механизм представлен на рисунке N2.
Как можно видеть на рисунке, запись очередной порции данных происходит в промежуточный массив строк фиксированной длины. Фиксированный размер массива и строк в нем позволяет исключить дорогостоящие операции выделения памяти. Это нисколько не снижает возможности такой системы. Достаточно выбрать длину строк и размер массива с запасом. Например, 5000 строк длиной в 4000 символов будет достаточно для отладки практически любой системы. А объем памяти в 20 мегабайт необходимый для этого, согласитесь, не критичен для современных систем. Если же массив все равно будет переполнен, то несложно предусмотреть механизм досрочной записи информации в файл.
Приведенный механизм обеспечивает практически моментальное выполнение функции WriteLog. Если в системе присутствуют ненагруженные процессорные ядра, то и запись в файл будет практически прозрачна для основного кода программы.
Преимущество описываемой системы в том, что она практически без изменений способна функционировать при отладке параллельной программы, когда в лог пишут сразу несколько потоков. Следует только добавить сохранение идентификатора процесса, чтобы потом можно было узнать, от каких потоков были получены сообщения (смотри рисунок N3).
Последнее усовершенствование, которое хочется предложить, это организация показа уровня вложенности сообщений при вызове функций или начале логического блока. Это можно легко организовать, используя специальный класс который в конструкторе записывает в лог идентификатор начала блока, а в деструкторе - идентификатор конца блока. Написав небольшую утилитку, можно трансформировать лог, опираясь на информацию об идентификаторах. Попробуем показать это на примере.
Код программы:
class NewLevel { public: NewLevel() { WriteLog("__BEGIN_LEVEL__n"); } ~NewLevel() { WriteLog("__END_LEVEL__n"); } }; #define NEW_LEVEL NewLevel tempLevelObject; void MyFoo() { WriteLog("Begin MyFoo()n"); NEW_LEVEL; int x = 5, y = 10; printf("Coordinate = (%d, %d)n", x, y); WriteLog("Begin Loop:n"); for (unsigned i = 0; i != 3; ++i) { NEW_LEVEL; WriteLog("i=%un", i); } } |
Содержимое лога:
Begin MyFoo() __BEGIN_LEVEL__ Coordinate = (5, 10) Begin Loop: __BEGIN_LEVEL__ i=0 __END_LEVEL__ __BEGIN_LEVEL__ i=1 __END_LEVEL__ __BEGIN_LEVEL__ i=2 __END_LEVEL__ Coordinate = (5, 10) __END_LEVEL__ |
Лог после трансформации:
Begin MyFoo() Coordinate = (5, 10) Begin Loop: i=0 i=1 i=2 Coordinate = (5, 10) |
Пожалуй, на этом можно закончить. Последнее о чем хочется еще упомянуть, это статья "Logging In C++" [11], которая также может Вам пригодиться. Желаем Вам удачной отладки.
Использование соответствующих аппаратной платформе базовых типов данных в языке Си/Си++ является важным элементом для создания качественных и высокопроизводительных программных решений. С приходом 64-битных систем начали использоваться новые модели данных - LLP64, LP64, ILP64 (см. таблицу N1), что изменило правила и рекомендации использования базовых типов данных. К таким типам можно отнести int, unsigned, long, unsigned long, ptrdiff_t, size_t и указатели. К сожалению, вопросы выбора типов практически не освещены в популярной литературе и статьях. А те источники, в которых они освещены, например "Software Optimization Guide for AMD64 Processors" [12], редко читают прикладные программисты.
Актуальность правильного выбора базовых типов для обработки данных обусловлена двумя важными причинами: корректностью работы кода и его эффективностью.
Исторически сложилось, что базовым и наиболее используемым целочисленным типом в языке Си и Си++ является int или unsigned int. Принято считать, что использование типа int является наиболее оптимальным, так как его размер совпадает с длиной машинного слова процессора. Машинное слово - это группа разрядов оперативной памяти, выбираемая процессором за одно обращение (или обрабатываемая им как единая группа), обычно содержит 16, 32 или 64 разряда.
Традиция делать размер типа int равным размеру машинного слова до недавнего времени нарушалась редко. На 16-битных процессорах int состоял из 16 бит. На 32-битных процессорах - 32 бита. Конечно, существовали и иные соотношения размера int и машинного слова, но они использовались редко и не представляют сейчас для нас интереса.
Нас интересует тот факт, что с приходом 64-битных процессоров размер типа int в большинстве систем остался равен 32-битам. Тип int имеет размер 32 бита в моделях данных LLP64 и LP64, которые используются в 64-битных операционных системах Windows и большинстве Unix систем (Linux, Solaris, SGI Irix, HP UX 11).
Оставить размер типа int равным 32-м битам является плохим решением по многим причинам, но оно является обоснованным выбором меньшего среди других зол. В первую очередь оно связано с вопросами обеспечения обратной совместимости. Более подробно о причинах такого выбора можно прочесть в блоге "Why did the Win64 team choose the LLP64 model?" и статью "64-Bit Programming Models: Why LP64?".
Для разработчиков 64-битных приложений все вышесказанное является предпосылкой придерживаться двух новых рекомендаций в процессе разработки программного обеспечения.
Рекомендация 1. Использовать для счетчиков циклов и адресной арифметики типы ptrdiff_t и size_t, вместо int и unsigned.
Рекомендация 2. Использовать для индексации в массивах типы ptrdiff_t и size_t, вместо int и unsigned.
Другими словами, по возможности использовать типы данных, которые на 64-битной системе имеют размер 64-бита. Отсюда следует утверждение, что не следует больше использовать конструкции вида:
for (int i = 0; i != n; i++) array[i] = 0.0; |
Да, это канонический пример кода. Да, его много во множестве программ. Да с него начинают обучению языку Си и Си++. Но больше его использовать не рекомендуется. Используйте либо итераторы, либо типы данных ptdriff_t и size_t, как показано в улучшенном примере:
for (size_t i = 0; i != n; i++) array[i] = 0.0; |
Разработчики Unix-приложений могут сделать замечание, что уже достаточно давно возникла практика использования типа long для счетчиков и индексации массивов. Тип long является 64-битным в 64-битных Unix-системах и его использование выглядит более элегантным, чем ptdriff_t или size_t. Да это так, но следует учесть два важных обстоятельства.
1) В 64-битных операционных системах Windows размер типа long остался 32-битным (см. таблицу N1). И, следовательно, он не может быть использован вместо типов ptrdiff_t и size_t.
2) Использование типов long и unsigned long еще больше усложняет жизнь разработчиков кросс-платформенных приложений для Windows и Linux систем. Тип long имеет в этих системах разный размер и только добавляет путаницы. Лучше придерживаться типов, имеющих одинаковый размер в 32-битных и 64-битных Windows и Linux системах.
Пришло время на примерах пояснить, почему так настойчиво рекомендуется отказаться от привычного использования типа int/unsigned в пользу ptrdiff_t/size_t.
Начнем мы с примера, демонстрирующего классическую ошибку использования типа unsigned для счетчика цикла в 64-битном коде. Мы уже описывали выше аналогичный пример, но повторим его еще раз в силу распространенности данной ошибки:
size_t Count = BigValue; for (unsigned Index = 0; Index != Count; ++Index) { ... } |
Это типичный код, варианты которого можно встретить во многих программах. Он корректно выполняется в 32-битных системах, где значение переменной Count не может превысить SIZE_MAX (который равняется в 32-битной системе UINT_MAX). В 64-битной системе диапазон возможных значений для Count может быть увеличен и тогда при значении Count > UINT_MAX возникнет вечный цикл. Корректным исправлением данного кода использование вместо типа unsigned типа size_t.
Следующий пример демонстрирует ошибку использования типа int для индексации больших массивов:
double *BigArray; int Index = 0; while (...) BigArray[Index++] = 3.14f; |
Этот код обычно не вызывает никаких подозрений у прикладного разработчика, привыкшего к практике использования в качестве индексов массивов переменные типа int или unsigned. К сожалению, приведенный код на 64-битной системе будет неработоспособен, если объем обрабатываемого массива BigArray превысит размер в четыре миллиарда элементов. В этом случае произойдет переполнение переменной Index, и результат работы программы будет некорректен (будет заполнен не весь массив). Корректировка кода вновь заключена в использовании для индексов типа ptrdiff_t или size_t.
В качестве последнего примера, хочется продемонстрировать потенциальную опасность смешенного использования 32-битных и 64-битных типов, которого следует по возможности избегать. К сожалению не многие разработчики задумываются, к чему может привести неаккуратная смешенная арифметика и для многих следующий пример оказывается неожиданностью (результаты получены с использованием Microsoft Visual C++ 2005, 64-битный режим компиляции):
int x = 100000; int y = 100000; int z = 100000; intptr_t size = 1; // Результат: intptr_t v1 = x * y * z; // -1530494976 intptr_t v2 = intptr_t(x) * y * z; // 1000000000000000 intptr_t v3 = x * y * intptr_t(z); // 141006540800000 intptr_t v4 = size * x * y * z; // 1000000000000000 intptr_t v5 = x * y * z * size; // -1530494976 intptr_t v6 = size * (x * y * z); // -1530494976 intptr_t v7 = size * (x * y) * z; // 141006540800000 intptr_t v8 = ((size * x) * y) * z; // 1000000000000000 intptr_t v9 = size * (x * (y * z)); // -1530494976 |
Хочется обратить внимание, что выражение вида "intptr_t v2 = intptr_t(x) * y * z;" вовсе не гарантирует правильный результат. Оно гарантирует только то, что выражение "intptr_t(x) * y * z" будет иметь тип intptr_t. Более подробно с этими вопросом поможет разобраться статья "20 ловушек переноса Си++ - кода на 64-битную платформу" [4].
Теперь перейдем к примеру, демонстрирующему преимущества использования типов ptrdiff_t и size_t с точки зрения производительности. Для демонстрации возьмем простой алгоритм вычисления минимальной длинны пути в алгоритме. С полным кодом программы можно познакомиться по ссылке: http://www.Viva64.com/articles/testspeedexp.zip.
В статье для краткости приведен только текст функций FindMinPath32 и FindMinPath64. Обе эти функции высчитывают длину минимального пути между двумя точками в лабиринте. Остальной код не представляет сейчас интереса.
typedef char FieldCell; #define FREE_CELL 1 #define BARRIER_CELL 2 #define TRAVERSED_PATH_CELL 3 unsigned FindMinPath32(FieldCell (*field)[ArrayHeight_32], unsigned x, unsigned y, unsigned bestPathLen, unsigned currentPathLen) { ++currentPathLen; if (currentPathLen >= bestPathLen) return UINT_MAX; if (x == FinishX_32 && y == FinishY_32) return currentPathLen; FieldCell oldState = field[x][y]; field[x][y] = TRAVERSED_PATH_CELL; unsigned len = UINT_MAX; if (x > 0 && field[x - 1][y] == FREE_CELL) { unsigned reslen = FindMinPath32(field, x - 1, y, bestPathLen, currentPathLen); len = min(reslen, len); } if (x < ArrayWidth_32 - 1 && field[x + 1][y] == FREE_CELL) { unsigned reslen = FindMinPath32(field, x + 1, y, bestPathLen, currentPathLen); len = min(reslen, len); } if (y > 0 && field[x][y - 1] == FREE_CELL) { unsigned reslen = FindMinPath32(field, x, y - 1, bestPathLen, currentPathLen); len = min(reslen, len); } if (y < ArrayHeight_32 - 1 && field[x][y + 1] == FREE_CELL) { unsigned reslen = FindMinPath32(field, x, y + 1, bestPathLen, currentPathLen); len = min(reslen, len); } field[x][y] = oldState; if (len >= bestPathLen) return UINT_MAX; return len; } size_t FindMinPath64(FieldCell (*field)[ArrayHeight_64], size_t x, size_t y, size_t bestPathLen, size_t currentPathLen) { ++currentPathLen; if (currentPathLen >= bestPathLen) return SIZE_MAX; if (x == FinishX_64 && y == FinishY_64) return currentPathLen; FieldCell oldState = field[x][y]; field[x][y] = TRAVERSED_PATH_CELL; size_t len = SIZE_MAX; if (x > 0 && field[x - 1][y] == FREE_CELL) { size_t reslen = FindMinPath64(field, x - 1, y, bestPathLen, currentPathLen); len = min(reslen, len); } if (x < ArrayWidth_64 - 1 && field[x + 1][y] == FREE_CELL) { size_t reslen = FindMinPath64(field, x + 1, y, bestPathLen, currentPathLen); len = min(reslen, len); } if (y > 0 && field[x][y - 1] == FREE_CELL) { size_t reslen = FindMinPath64(field, x, y - 1, bestPathLen, currentPathLen); len = min(reslen, len); } if (y < ArrayHeight_64 - 1 && field[x][y + 1] == FREE_CELL) { size_t reslen = FindMinPath64(field, x, y + 1, bestPathLen, currentPathLen); len = min(reslen, len); } field[x][y] = oldState; if (len >= bestPathLen) return SIZE_MAX; return len; } |
Функция FindMinPath32 написана в классическом 32-бином стиле с использованием типов unsigned. Функция FindMinPath64 отличается от нее только тем, что в ней все типы unsigned заменены на типы size_t. Других отличий нет! Согласитесь, что это не является сложной модификацией программы. А теперь сравним скорости выполнения этих двух функций (см. таблицу N2).
Режим и функция. | Время работы функции | |
1 | 32-битный режим сборки. Функция FindMinPath32 | 1 |
2 | 32-битный режим сборки. Функция FindMinPath64 | 1.002 |
3 | 64-битный режим сборки. Функция FindMinPath32 | 0.93 |
4 | 64-битный режим сборки. Функция FindMinPath64 | 0.85 |
В таблице N2 показано приведенное время относительно скорости выполнения функции FindMinPath32 на 32-битной системе. Это сделано для большей наглядности.
В первой строке время работы функции FindMinPath32 на 32-битной системе равно 1. Это вызвано тем, что мы взяли как раз ее время работы за единицу измерения.
Во второй строке мы видим, что время работы функции FindMinPath64 на 32-битной системе также равно 1. Это не удивительно, так как на 32-битной системе тип unsigned совпадает с типом size_t и никакой разницы между функцией FindMinPath32 и FindMinPath64 нет. Небольшое отклонение (1.002) говорит только о небольшой погрешности в измерениях.
В третье строке мы видим прирост производительности равный 7%. Это вполне ожидаемый результат от перекомпиляции кода для 64-битной системы.
Наибольший интерес представляет 4 строка. Прирост производительности составляет 15%. Это значит, что простое использование типа size_t вместо unsigned позволяет компилятору построить более эффективный код, работающий еще на 8% быстрее!
Это простой и наглядный пример, когда использование данных, не равных размеру машинного слова снижает производительность алгоритма. Простая замена типов int и unsigned на типы ptrdiff_t и size_t может дать существенный прирост производительности. В первую очередь это относится к использованию этих типов данных для индексации массивов, адресной арифметики и организации циклов.
Хочется надеяться после всего вышесказанного, Вы задумаетесь, стоит ли продолжать писать:
for (int i = 0; i !=n; i++) array[i] = 0.0; |
Для автоматизации поиска ошибок в 64-бином коде, разработчики Windows-приложений могут обратить внимание в сторону статического анализатора кода Viva64 [8]. Во-первых, его использование позволит выявить большинство ошибок. Во-вторых, разрабатывая программы под его контролем, Вы станете реже использовать 32-битных переменные, будете избегать смешанной арифметики с 32-битными и 64-битными типами данных, что автоматически увеличит производительность Вашего кода. Для разработчиков под Unix системы интерес могут представлять статические анализаторы Gimpel Software PC-Lint и Parasoft C++test. Они способны диагностировать ряд 64-битных ошибок в коде с моделью данных LP64, используемой в большинстве Unix-систем.
Более подробно, Вы можете познакомиться с вопросами разработки качественного и эффективного 64-битного кода в следующих статьях: "Проблемы тестирования 64-битных приложений", "24 Considerations for Moving Your Application to a 64-bit Platform", "Porting and Optimizing Multimedia Codecs for AMD64 architecture on Microsoft Windows", "Porting and Optimizing Applications on 64-bit Windows for AMD64 Architecture".
В последней части этой статьи хочется рассмотреть еще несколько технологий, которые могут быть Вам полезны при разработке ресурсоемких программных решений.
Intrinsic-функции это специальные системно-зависимые функции, выполняющие действия, которые невозможно выполнить на уровне Си/Си++ кода или которые выполняют эти действия намного эффективнее. По сути, они позволяют избавиться от использования inline-ассемблера, т.к. его использование часто нежелательно или невозможно.
Программы могут использовать intrinsic-функции для создания более быстрого кода за счет отсутствия накладных расходов на вызов обычного вида функций. При этом, естественно, размер кода будет чуть-чуть больше. В MSDN приводится список функций, которые могут быть заменены их intrinsic-версией. Это, например, memcpy, strcmp и другие.
В компиляторе Microsoft Visual C++ есть специальная опция "/Oi", которая позволяет автоматически заменять вызовы некоторых функций на intrinsic-аналоги.
Помимо автоматической замены обычных функций на intrinsic-варианты, можно явно использовать в коде intrinsic-функции. Вот для чего это может быть нужно:
Использование intrinsic-функций в автоматическом режиме (с помощью ключа компилятора) позволяет получить бесплатно несколько процентов прироста производительности, а "ручное" - даже больше. Поэтому использование intrinsic-функций вполне оправдано.
Более подробно с применением intrinsic-функций можно ознакомиться в блоге команды Visual C++ [21].
Выравнивание данных в последнее время не так сильно сказывается на производительности кода, как, скажем, 10 лет назад. Однако иногда и тут можно получить дополнительный выигрыш в экономии памяти и производительности.
Рассмотрим пример:
struct foo_original {int a; void *b; int c; }; |
В 32-битном режиме данная структура занимает 12 байт, но в 64-битном - уже 24 байта. Для того чтобы в 64-битном режиме структура занимала положенные ей 16 байт следует изменить порядок следования полей:
struct foo_new { void *b; int a; int c; }; |
В некоторых случаях полезно явно помогать компилятору, задавая выравнивание вручную, чтобы увеличить производительность. Например, данные SSE должны быть выровнены по границе 16 байт. Вот как этого можно добиться:
// 16-byte aligned data __declspec(align(16)) double init_val [3.14, 3.14]; // SSE2 movapd instruction _m128d vector_var = __mm_load_pd(init_val); |
Источники "Porting and Optimizing Multimedia Codecs for AMD64 architecture on Microsoft Windows", " Porting and Optimizing Applications on 64-bit Windows for AMD64 Architecture" [20] дают подробный обзор данных вопросов.
С появлением 64-битных систем технология отображения файлов в память стала более привлекательной в использовании, так как увеличилось окно доступа к данным. Для некоторых приложений это может быть очень полезным приобретением. Не забывайте о нем.
Одна из наиболее серьезных проблем для компилятора - это совмещение (aliasing) имен. Когда код читает и пишет память, часто на этапе компиляции невозможно определить, получает ли к данной области памяти доступ более чем один указатель. То есть, может ли более чем один указатель "синонимом" для одной и той же области памяти. Поэтому, например, внутри цикла, в котором и читается, и пишется память, компилятор должен быть очень осторожен с хранением данных в регистрах, а не в памяти. Это недостаточно активное использование регистров может существенно повлиять на производительность.
Ключевое слово __restrict используется для того, чтобы облегчить компилятору принятие решения. Оно говорит компилятору "быть смелее" с использованием регистров.
Ключевое слово __restrict позволяет компилятору не считать отмеченные указатели синонимичными (aliased), то есть ссылающимися на одну и ту же область памяти. Компилятор в таком случае может произвести более эффективную оптимизацию. Рассмотрим пример:
int * __restrict a; int *b, *c; for (int i = 0; i < 100; i++) { *a += *b++ - *c++ ; // no aliases exist } |
В данном коде компилятор может безопасно хранить сумму в регистре, связанном с переменной "a", избегая записи в память. Хорошим источником информации об использовании ключевого слова __restrict является MSDN.
Приложения, запускаемые на 64-битных процессорах (независимо от режима) будут работать более эффективно, если в них используются SSE-инструкции вместо MMX/3DNow. Это связанно с разрядностью обрабатываемых данных. SSE/SSE2 инструкции оперируют 128-битными данными, в то время как MMX/3DNow - только лишь 64-битными. Поэтому код, использующих MMX/3DNow, лучше переписать с ориентацией на SSE.
В данной статье мы не будем останавливаться на SSE-инструкциях, отсылая интересующихся читателей к документации от разработчиков процессорных архитектур.
64-битная архитектура приносит новые возможности для оптимизации на уровне отдельных операторов языка программирования. Это уже ставшие традиционными приемы по "переписыванию" кусочков программы с тем, чтобы компилятор еще лучше их оптимизировал. Рекомендовать к массовому использованию эти приемы, конечно же, не стоит, но знать о них может быть полезно.
На первом месте из целого списка данных оптимизаций стоит ручное разворачивание циклов (unroll the loop). Суть данного метода легко увидеть из примера:
double a[100], sum, sum1, sum2, sum3, sum4; sum = sum1 = sum2 = sum3 = sum4 = 0.0; for (int i = 0; i < 100; I += 4) { sum1 += a[i]; sum2 += a[i+1]; sum3 += a[i+2]; sum4 += a[i+3]; } sum = sum1 + sum2 + sum3 + sum4; |
Во многих случаях, компилятор сам может развернуть цикл до такого представления (ключ /fp:fast для Visual C++), но не всегда.
Другой синтаксической оптимизацией является использование массивной (от слова "массив") нотации вместо указательной (от слова "указатель").
Множество подобных приемов приведено в "Software Optimization Guide for AMD64 Processors".
Несмотря на то, что при создании программных систем, эффективно использующих аппаратные возможности современной вычислительной техники, придется столкнуться с большим количеством трудностей, игра стоит свеч. Параллельные 64-битные системы открывают новые возможности в построении настоящих масштабируемых решений. Они позволяют поднять на новый уровень возможности современных программных средств в обработке данных, будь то игры, CAD-системы или распознавание образов. Желаем Вам удачи в освоении новых технологий!
Главная страница - Программные продукты - Статьи - Разработка ПО, Microsoft |
Распечатать »
Правила публикации » |
Написать редактору | |||
Рекомендовать » | Дата публикации: 23.11.2009 | |||
|
Новости по теме |
Одобрены первые программы цифровой трансформации ведомств на 2025 год
|
Рассылки Subscribe.ru |
Статьи по теме |
Новинки каталога Download |
5 бесплатных приложений, которые будут напоминать вам отдохнуть от экрана компьютера или смартфона
|
Исходники |
Отдам код в хорошие руки. Мошенничество в ИТ-сфере
|
Документация |