10 неочевидных новичку подводных камней программирования


Оглавление (нажмите, чтобы открыть):

Подводные камни

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

Как я уже говорил, при объявлении переменных в Паскале нужно обязательно указывать их типы. В некоторых других языках, таких как Visual Basic Script, JavaScript и т.п. это делать не обязательно. Но мы тренируемся на Паскале, поэтому вам нужно будет научиться заранее думать, где вы будете использовать объявленную переменную, чтобы выбрать для неё правильный тип.

Что же будет, если вы ошибётесь с выбором типа данных?

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

Однако есть более неприятные ситуации. Допустим, у нас есть такая программа:

Как вы думаете, сколько будет 0+300? Правильно, 300. Однако это ваша точка зрения. А вот с точки зрения данной программы, 0 + 300 = 44. Не верите? Посмотрите рис. 14.1.

Рис. 14.1. Результат работы программы.

Почему так получилось? Давайте разберёмся.

Итак, переменная x у нас имеет тип Byte. Как вы уже знаете, переменная данного типа может принимать значения ТОЛЬКО в диапазоне 0…255. Поэтому число 300 «впихнуть» в эту переменную никак не получится!

Однако компилятор не выдаёт ошибки! Почему? Да потому что компилятор не знает, чего хочет программист. Может быть программист хочет, чтобы результат был именно 44, а не 300.

Но откуда взялось число 44?

Сначала переменная х хранила 0. Мы попытались прибавить к этому значению 300. Но переменная х не может хранить число, значение которого более 255. Поэтому к 0 прибавляется 255, а затем разность между 300 и 255.

Однако 300 – 255 = 45. Почему же тогда результат равен 44? А потому что значения начинаются с нуля. То есть 45-е число в диапазоне 0…255 это и будет 44. Если не верите, то можете посчитать.

Конечно, это упрощённые объяснения для начинающих. Однако пока этого достаточно. Если хотите знать больше, то изучайте информатику и системы счисления. Главное, что вы должны усвоить из этого урока – надо быть внимательным при выборе типов данных, и помнить о том, что результат какой-либо операции может не поместиться в тип данных, который имеет переменная, в которую записывается этот результат. А это, в свою очередь, может привести (и обязательно приведёт) к ошибкам в программе. К тому же такие ошибки довольно трудно искать.

Разумеется, это не все проблемы, с которыми может столкнуться программист. Это только начало проблем)))

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

10 подводных камней, подстерегающих при создании лэндингов

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

1. Время создания сайта может оказаться неприемлемым
Это для Вас время деньги, а для подрядчика это может быть и не так. Знаете сколько в среднем фрилансер создает хороший сайт? Месяц! У Вас есть этот месяц.

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

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

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

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

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

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

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

9. Сложная система администрирования
Стрельба из пушки по воробьям очень распространенная забава среди разработчиков сайтов. Зачастую заманивая фразой «широкая функциональность», Вас обрекают на много часовое чтение документации и надоедающую игру с настройками сайта

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

10 подводных камней для начинающего SEO-оптимизатора

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

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

О чем же должен знать любой новичок, прежде чем приступить к работе?

Оптимизация – это систематический труд. То есть делать что-то придется часто и регулярно, чтобы не потерять свои достижения. Сайт будет требовать постоянной поддержки, особенно учитывая то, что алгоритмы поисковых систем очень изменчивы. Необходимо постоянно отслеживать изменения в алгоритмах поисковых систем и быть в курсе последних тенденций в области СЕО. Для этого нужно читать блоги seo-оптимизаторов и посещать популярные seo-форумы. Это как из книги «Алиса в зазеркалье» — нужно быстро бежать, чтобы хотя бы просто остаться на месте.

Нужно хорошо понимать цели и задачи. Прежде чем что-то сделать, хорошо бы ответить на простой вопрос: «зачем?». Поддерживайте коммуникацию с заказчиком, задавайте наводящие вопросы. Для чего нужен сайт, велика ли конкуренция в этом сегменте и т. д.

Не стоит игнорировать мета-теги. Особенно «Title». На этом, пожалуй, чаще всего попадаются начинающие. Для некоторых поисковых систем именно «Title» имеет первостепенное значение, так что грамотно заполняйте это поле и никогда не оставляйте его пустым.

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

Ссылка ссылке рознь. Каждый знает, что чем больше ссылок – тем «ценнее» сайт для поисковой системы. Но все не может быть настолько просто! Тенденция такова, что все большее значение придается деталям и максимальной «естественности». Важны тематика ссылок, дата создания, репутация партнеров и т. д. Так что не торопитесь скупать «ссылочную массу» по выгодным ценам. Это тоже возможность получить клеймо «спам».

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

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

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

Стоит активнее пользоваться перелинковкой! Для этого достаточно использовать контекстные ссылки (это не сложно сделать вручную еще на этапе создания контента). Или добавить целый блок внизу страницы (вроде «с этим товаром покупают», «вам также может быть интересно»).

Обязательно оценивать сайт с точки зрения юзабилити, т. е. удобства для пользователя. Логична ли структура? Легко ли найти нужный раздел? А прайс? А контактную информацию? Грамотно ли организован рубрикатор каталога? Чем удобнее пользователю будет ориентироваться на сайте, тем выше шансы, что он найдет необходимую информацию или совершит покупку.

Подсчитать минимальное количество действий, которые надо совершить обезьянке, чтобы получить кучу из n камней

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

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

Формат ввода
Строка, содержащая число n — необходимое количество камней в куче.

Формат вывода
Число — необходимое количество шагов.

Пример 1
Ввод
11

Вывод
5
Пример 2
Ввод
3
Вывод
2

Есть код, но когда я к примеру ввожу 4, то программа ничего не делает(то есть просто зависла)

32 подводных камня OpenMP при программировании на Си++

Аннотация

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

Введение

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

В связи с тем, что параллельное программирование начинает набирать популярность только сейчас, процесс распараллеливания существующего приложения или написания нового параллельного кода может вызвать проблемы даже для опытных программистов, так как данная область является для них новой. Существующие компиляторы и анализаторы кода позволяют диагностировать лишь некоторые из возможных ошибок. Остальные ошибки (а таких большинство) остаются неучтенными и могут существенно увеличить время тестирования и отладки, особенно с учетом того, что такие ошибки почти всегда воспроизводятся нестабильно. В данной статье рассматривается язык Си++, поскольку к коду именно на этом языке чаще всего предъявляются требования высокой производительности. Так как поддержка технологии OpenMP встроена в Microsoft Visual Studio 2005 и 2008 (заявлено соответствие стандарту OpenMP 2.0), мы будем рассматривать именно эту технологию. OpenMP позволяет с минимальными затратами переделать существующий код — достаточно лишь включить дополнительный флаг компилятора /openmp и добавить в свой код соответствующие директивы, описывающие, как именно выполнение программы будет распределено на несколько процессоров.

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

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

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

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

Директивами (directives) назовем собственно директивы OpenMP, определяющие способ распараллеливания кода. Все директивы OpenMP имеют вид #pragma omp .

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

Параллельной секцией (section) назовем фрагмент кода, на который распространяется действие директивы #pragma omp parallel.

Данная статья предполагает, что читатель уже знаком с основами OpenMP и собирается применять эту технологию в своих программах. Если же читатель еще не знаком с OpenMP, для первоначального знакомства можно посмотреть документ [2]. Более подробное описание директив, выражений, функций и глобальных переменных OpenMP можно найти в спецификации OpenMP 2.0 [3]. Также эта спецификация продублирована в справочной системе MSDN Library, в более удобной форме, чем формат PDF.

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

Логические ошибки

1. Отсутствие /openmp

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

Поддержку OpenMP можно включить в диалоговом окне свойств проекта (раздел «Configuration Properties | C/C++ | Language»).

2. Отсутствие parallel

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

Первый фрагмент кода успешно скомпилируется, и директива #pragma omp for будет попросту проигнорирована компилятором. Таким образом, цикл, вопреки ожиданиям программиста, будет выполняться только одним потоком, и обнаружить это будет достаточно затруднительно. Помимо директивы #pragma omp parallel for, эта ошибка может возникнуть и с директивой #pragma omp parallel sections.

3. Отсутствие omp

Ситуация, аналогичная предыдущей, возникнет, если в директиве OpenMP не написать ключевое слово omp [4]. Рассмотрим простой пример.

При выполнении этого кода сообщение «me» будет выведено два раза вместо ожидаемого одного. При компиляции компилятор выдаст предупреждение: «warning C4068: unknown pragma». Однако предупреждения могут быть отключены в настройках проекта, либо предупреждение может быть проигнорировано программистом.

4. Отсутствие for

Директива #pragma omp parallel может относиться как к блоку кода, так и к одной команде. В случае цикла for это может приводить к неожиданному поведению:

Если программист хотел распределить выполнение этого цикла на два потока, ему следовало использовать директиву #pragma omp parallel for. В этом случае цикл выполнился бы 10 раз. Однако, при запуске приведенного выше кода цикл будет выполняться по одному разу в каждом потоке, и вместо ожидаемых 10 раз функция myFunc будет вызвана 20 раз. Исправленная версия кода должна выглядеть следующим образом:

5. Ненужное распараллеливание

Применение директивы #pragma omp parallel к большому участку кода при невнимательности программиста может вызывать неожиданное поведение, аналогичное предыдущему случаю:

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

6. Неправильное применение ordered

Применение директивы ordered может вызвать проблемы у начинающих программистов, не слишком хорошо знакомых с OpenMP [1]. Приведем пример кода:

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

7. Переопределение количества потоков внутри параллельной секции

Перейдем к рассмотрению более сложных ошибок, которые могут возникнуть при недостаточном знании стандарта OpenMP. Согласно спецификации OpenMP 2.0 [3], количество потоков нельзя переопределять внутри параллельной секции. В Си++ это приводит к ошибкам во время выполнения программы и ее аварийному завершению. Пример:

8. Попытка использовать блокировку без инциализации переменной

Согласно спецификации OpenMP 2.0 [3], переменные, использующиеся для блокировки, необходимо инциализировать перед использованием, вызвав функцию omp_init_lock или omp_init_nest_lock (в зависимости от типа переменной). В С++ попытка использования (установка блокировки, снятие блокировки, проверка блокировки) неинициализированной переменной приводит к ошибке во время выполнения программы.

9. Попытка снять блокировку не из того потока, который ее установил

Если блокировка установлена одним потоком, попытка ее снятия из другого потока может привести к непредсказуемым результатам [3]. Рассмотрим пример:

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

10. Попытка использования блокировки как барьера


Функция omp_set_lock блокирует выполнение потока до тех пор, пока переменная, использующаяся для блокировки, не станет доступна, то есть до тех пор, пока тот же самый поток не вызовет функцию omp_unset_lock. Следовательно, как уже говорилось в описании предыдущей ошибки, каждый поток должен содержать вызовы обеих функций. Однако, начинающий программист, недостаточно хорошо понимающий принципы работы OpenMP, может попытаться использовать функцию omp_set_lock в качестве барьера, то есть вместо директивы #pragma omp barrier (эту директиву нельзя использовать внутри параллельных секций, к которым применяется директива #pragma omp sections). В результате возникнет следующий код.

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

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

11. Зависимость поведения от количества потоков

Количество параллельных потоков, создаваемых при выполнении приложения, в общем случае не является постоянной величиной. По умолчанию оно, как правило, равняется числу установленных на компьютере процессоров. Однако, число потоков может также задаваться программистом вручную (например, с помощью функции omp_set_num_threads, или выражения num_threads, которое имеет больший приоритет, чем эта функция). Помимо указанной функции и выражения существует еще и переменная среды OMP_NUM_THREADS, имеющая наименьший приоритет. Следовательно, число потоков является весьма ненадежным числом, к тому же значение этого числа по умолчанию может оказаться разным на разных компьютерах. Поведение вашего кода ни в коем случае не должно зависеть от количества выполняющих его потоков, если только вы не уверены до конца в том, что вам это действительно нужно.Рассмотрим пример некорректного кода, взятый из статьи [5]. Приведенная ниже программа должна по замыслу программиста вывести на экран все буквы английского алфавита.Некорректно:

На практике, однако, будет выведено только 24 буквы из 26. Причина проблемы заключается в том, что 26 (число букв) делится на 4 (число потоков) с остатком. Следовательно, оставшиеся две буквы не будут выведены на экран. В качестве исправления можно либо отказаться от использования количества потоков в вычислениях и переписать код иначе, либо распределить вычисления на корректное число потоков (например, 2). Допустим, программист решил отказаться от использования количества потоков в своих вычислениях и возложить распределение работы между потоками на компилятор. В этом случае исправленная версия кода будет иметь следующий вид.Корректно:

Все итерации будут гарантированно выполнены. Если необходимо задать способ распределения итераций между потоками, можно воспользоваться выражением schedule. Распределением работы между потоками теперь занимается компилятор, и он не забудет про две «лишние» итерации. К тому же, код оказался намного короче и понятнее.

12. Некорректное использование динамического создания потоков

В OpenMP слово dynamic встречается в двух контекстах: в выражении schedule(dynamic) и в переменной среды OMP_DYNAMIC, что вносит некоторую путаницу. Важно понимать разницу между этими двумя случаями и не считать, что выражением schedule(dynamic) можно пользоваться, только если переменная OMP_DYNAMIC имеет значение true. На самом деле эти случаи никак друг с другом не связаны.

Выражение schedule(dynamic) означает, что поделенные на куски (chunks) итерации цикла будут распределяться между потоками динамически — освободившись, поток будет брать следующую «порцию». В частности, в предыдущем примере это означало бы, что каждый из четырех потоков выведет по 6 букв, после чего тот поток, который освободится первым, выведет последние 2 буквы.

Переменная OMP_DYNAMIC задает возможность динамического определения числа потоков компилятором. Проблема заключается в том, что эта переменная имеет больший приоритет, чем даже выражение num_threads. Следовательно, если выполнение кода как-то зависит от количества выполняющих его потоков, поведение может стать некорректным, и это — еще один аргумент в пользу того, чтобы не писать зависящий от количества потоков код.Опыт показывает, что в Visual Studio 2008 переменная OMP_DYNAMIC по умолчанию имеет значение false. Однако, нет никаких гарантий, что это не изменится в будущем. В спецификации OpenMP [3] сказано, что значение этой переменной зависит от конкретной реализации. Следовательно, если в предыдущем примере программист решил пойти по легкому пути и все же использовать в своих вычислениях число потоков, вместо того, чтобы переписывать код, ему следует позаботиться о том, что это число потоков будет именно таким, как он указал. В противном случае на четырехпроцессорной машине код начнет работать некорректно.

Мастер Йода рекомендует:  Качественная архитектура ПО на примере концепции Linux «всё есть файл»

13. Одновременное использование общего ресурса

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

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

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

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

14. Незащищенный доступ к общей памяти

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

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

При таком изменении кода в случае двух потоков будет выведена строка «a=2».

15. Использование директивы flush с указателем

Директива flush служит для того, чтобы потоки обновили значения общих переменных. Например, если один поток установил значение доступной обоим потокам общей переменной а, равное 1, это не гарантирует, что другой поток, обратившись к той же переменной, получит значение 1. Еще раз подчеркнем, что данная директива обновляет именно значения переменных. Если в коде имеется доступный обоим потокам указатель на какой-либо объект, вызов директивы flush обновит только значение этого указателя, но не состояние объекта. Более того, в стандарте OpenMP [3] явно сказано, что переменная-аргумент директивы flush не должна быть указателем.

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

16. Отсутствие директивы flush

Согласно спецификации OpenMP [3], эта директива неявно выполняется во многих случаях (полный список этих случаев будет приведен ниже, в описании одной из ошибок производительности). Понадеявшись на это, программист может не использовать эту директиву там, где она действительно необходима. Директива flush не выполняется в следующих случаях:

  • При входе в параллельную секцию директивы for.
  • При входе и при выходе из секции директивы master.
  • При входе в параллельную секцию директивы sections.
  • При входе в секцию директивы single.
  • При выходе из секции директивы for, single или sections, если к директиве применено выражение nowait (вместе с неявным барьером эта директива убирает и неявную директиву flush).

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

17. Отсутствие синхронизации

Помимо необходимости применения директивы flush, программист должен помнить еще и о синхронизации потоков, не полагаясь при этом на OpenMP.

Исправленная версия кода из предыдущего примера отнюдь не гарантирует, что в консольное окно будет выведено число «2». Да, выполняющий секцию поток выведет значение переменной а, актуальное на момент вывода. Однако, нет никакой гарантии того, что потоки войдут в секцию single одновременно. В общем случае может быть взято как значение «1», так и «2». Такое поведение вызвано отсутствием синхронизации потоков. Директива single означает лишь то, что соответствующая секция должна быть выполнена одним потоком. А этим потоком с равной вероятностью может оказаться и тот, который завершился первым. Тогда на экран будет выведена цифра «1». Аналогичная ошибка описана в статье [6].

Неявная синхронизация потоков в виде директивы barrier выполняется только при выходе из секции директивы for, single или sections, если к директиве не применено выражение nowait, которое отменяет неявную синхронизацию. Во всех остальных случаях программист должен заботиться о синхронизации сам.

Эта версия кода является полностью корректной — на экран всегда будет выведена цифра «2». Отметим, что в этой версии кода директива flush отсутствует — она неявно включена в директиву barrier.

Теперь рассмотрим еще один весьма интересный пример отсутствия синхронизации, взятый из MSDN [7].

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

18. Внешняя переменная задана как threadprivate не во всех модулях

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

Прежде чем перейти к рассмотрению конкретных ошибок отметим, что практически все они, так или иначе, связаны с переменными, являющимися параметрами выражения private (а также его вариаций — директивы threadprivate и выражений firstprivate и lastprivate). Большинство этих ошибок можно избежать, если отказаться от директивы threadprivate и выражения private, и вместо этого просто объявлять соответствующие переменные как локальные переменные в параллельных секциях.

Теперь, когда вы предупреждены, перейдем к рассмотрению конкретных ошибок. Начнем с директивы threadprivate. Эта директива применяется, как правило, к глобальным переменным, в том числе и к внешним переменным, объявленным в другом модуле. В этом случае директива должна быть применена к переменной во всех модулях, в которых она встречается. Это правило описано в уже упомянутой выше статье MSDN [7].

Частным случаем этого правила является другое правило, приведенное в той же статье: директиву threadprivate нельзя применять к переменным, объявленным в динамически подключаемой библиотеке, которая будет загружаться с помощью функции LoadLibrary или ключа линковщика /DELAYLOAD (в этом случае функция LoadLibrary используется неявно).

19. Неинициализированные локальные переменные

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

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

20. Забытая директива threadprivate

Поскольку директива threadprivate применяется к переменной лишь единожды, и обычно она используется для глобальных переменных, объявленных в начале модуля, про нее легко забыть — например, когда возникает необходимость что-то изменить в большом модуле, созданном полгода назад. В результате глобальная переменная, вопреки ожиданиям программиста, может оказаться не общей, как должно быть по умолчанию, а локальной для потока. Согласно спецификации OpenMP [3], значения таких переменных после соответствующей параллельной секции являются непредсказуемыми.

Поведение приведенной выше программы будет полностью соответствовать документации — иногда в консольное окно будет выводиться число «6» — то самое значение, которое ожидает программист, а иногда — «0», что, в принципе, более логично, так как именно это значение было присвоено переменной до входа в параллельную секцию. Теоретически такое же поведение должно наблюдаться, если вместо директивы threadprivate режим доступа к переменной «а» будет определяться выражениями private или firstprivate. Но на практике с компилятором среды Visual Studio 2008 описанное поведение удалось воспроизвести только с директивой threadprivate, именно поэтому этот пример и приведен в данной статье. К тому же такой вариант наиболее вероятен. Тем не менее, это не означает, что в других реализациях поведение будет корректным, поэтому следует учитывать и другие два варианта.

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

В этой версии кода переменная «a» как бы становится общей на время выполнения определенной параллельной секции. Конечно, это решение не является лучшим, но за то оно гарантирует, что старый код не изменит своего поведения.

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

21. Забытое выражение private

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

На первый взгляд может показаться, что эта ошибка эквивалентна предыдущей, но это не так. В предыдущем случае вывод производился после параллельной секции, а здесь значение переменной берется внутри параллельной секции. В результате, если значение переменной перед циклом равно нулю, в случае двухпроцессорной машины, вместо ожидаемого числа «10» в консольное окно будет выведено число «5», потому что работа будет разделена пополам между потоками. Следовательно, каждый поток увеличит свое локальное значение переменной a лишь пять раз вместо ожидаемых десяти раз. Более того, значение переменной, получается, будет зависеть от количества потоков, выполняющих код параллельной секции. Кстати, эта ошибка возникнет и в случае использования выражения firstprivate вместо private.

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

22. Некорректное распараллеливание работы с локальными переменными

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

В данном случае программист хотел увеличить значение локальной копии переменной в каждом из потоков на 101 и для этого использовал директиву sections. Однако, поскольку в директиве отсутствовало слово parallel, дополнительного распараллеливания не произошло. Работа распределилась на те же потоки. В результате на двухпроцессорной машине один поток выведет «1», а другой выведет «100». При большем количестве потоков результаты будут еще более неожиданными для программиста. Отметим, что если бы переменная «а» не была локальной (private), такой код был бы вполне корректным. Однако в случае локальной переменной секции необходимо распараллелить дополнительно.

23. Неосторожное применение lastprivate

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

Такой код теоретически может привести к ошибке. На практике это воспроизвести не удалось. Однако, это не значит, что ошибка не возникнет никогда.

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

24. Непредсказуемые значения threadprivate-переменных в начале параллельных секций

Эта ошибка описана в спецификации OpenMP [3]. Если изменить значение переменной, объявленной как threadprivate, начальное значение переменной в параллельной секции окажется непредсказуемым.

К сожалению, пример, приведенный в спецификации, не компилируется в Visual Studio, потому что компилятор этой среды не поддерживает динамическую инициализацию переменных, фигурирующих в директиве threadprivate. Поэтому приведем свой собственный, менее сложный, пример.

В результате выполнения этого кода один из потоков выведет значение 5, а другой выведет 10. Если убрать инициализацию переменной а до директивы threadprivate, один из потоков начнет выводить 0, а второй — по-прежнему 10. Избавиться от непредсказуемого поведения удастся лишь убрав второе присваивание. В этом случае оба потока будут выводить значение 5 (если первое присваивание все же оставить). Конечно, такие исправления изменят поведение кода. Они описаны здесь лишь для того, чтобы более четко показать поведение OpenMP в данном случае. Вывод здесь может быть только один: никогда не полагайтесь на компилятор в вопросах инициализации локальных переменных. В случае private и lastprivate попытка использования неинициализированных переменных вызовет уже описанную ранее ошибку во время выполнения программы, которую, по крайней мере, можно относительно легко локализовать. Но директива threadprivate, как видите, может давать непредсказуемые результаты без всяких ошибок. Вообще от использования этой директивы лучше всего отказаться. В этом случае код станет намного более предсказуемым и понятным.

25. Некоторые ограничения локальных переменных

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

  • Переменная в выражении private не должна иметь ссылочный тип.
  • Если переменная в выражении lastprivate является экземпляром класса, в этом классе должен быть определен конструктор копирования.
  • Переменная в выражении firstprivate не должна иметь ссылочный тип.
  • Если переменная в выражении firstprivate является экземпляром класса, в этом классе должен быть определен конструктор копирования.
  • Переменная в выражении threadprivate не должна иметь ссылочный тип.

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

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

26. Локальные переменные не помечены как таковые

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

Для диагностики этой ошибки рекомендуется уже упоминавшееся выше использование выражения default(none).

Как видно, данная ошибка весьма обобщенная, и привести конкретный пример достаточно трудно. Однако, в одной из статьей [9] описана ситуация, в которой эта ошибка проявляется вполне явно.

Смысл программы очень прост — массив записей с двумя полями заполняется из двух потоков. Один поток заполняет одно поле записей, другой — другое. Потом делается проверка — все ли записи заполнены правильно.

Ошибка здесь заключается в том, что в цикле для счетчика используется общая переменная i. В результате при выполнении программы в некоторых случаях будет выводиться сообщение «OpenMP Error!», в других — возникать ошибка access violation, и лишь изредка будет выводиться «OK!». Для того, чтобы исправить эту ошибку, достаточно объявить переменную-счетчик как локальную переменную.

В статье [1] имеется аналогичный пример, касающийся именно циклов for (он выделен как отдельная ошибка). Там сказано, что переменная-счетчик для цикла for, к которому применяется директива OpenMP for, должна быть объявлена как локальная. С первого взгляда может показаться, что там рассматривается абсолютно такая же ситуация, однако, это не так.

Дело в том, что, согласно станадрту OpenMP, переменная, использующаяся в качестве счетчика в цикле, неявно преобразуется из общей в локальную, даже если она является параметром выражения shared. Компилятор, делая это преобразование, не выдает никаких предупреждений. Именно этот случай описан в статье [1] и в этой ситуации преобразование действительно делается. Однако, в нашем примере вместо директивы for используется директива sections, и такое преобразование не производится.

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

27. Параллельная работа с массивом без упорядочивания итераций

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

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

Ошибки производительности

1. Ненужная директива flush

Все рассмотренные ранее ошибки влияли на поведение программы и являлись в той или иной мере критическими. Теперь же рассмотрим ошибки, которые повлияют лишь на производительность программы, не затрагивая логики ее работы. Эти ошибки описаны в статье [1].

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

Приведем случаи, в которых эта директива присутствует неявно и использовать ее нет нужды:

  • В директиве barrier
  • При входе и при выходе из параллельной секции директивы critical
  • При входе и при выходе из параллельной секции директивы ordered
  • При входе и при выходе из параллельной секции директивы parallel
  • При выходе из параллельной секции директивы for
  • При выходе из параллельной секции директивы sections
  • При выходе из параллельной секции директивы single
  • При входе и при выходе из параллельной секции директивы parallel for
  • При входе и при выходе из параллельной секции директивы parallel sections

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

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

Здесь х — скалярная переменная, expr — выражение со скалярными типами, в котором не присутствует переменная х, binop — не перегруженный оператор +, *, -, /, &, ^, |, >. Во всех остальных случаях применять директиву atomic нельзя (это проверяется компилятором).


Вообще, с точки зрения убывания быстродействия, средства защиты общих данных от одновременной записи располагаются так: atomic, critical, omp_set_lock.

3. Ненужная защита памяти от одновременной записи

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

Переменную не нужно защищать от одновременной записи в следующих случаях:

  • Если переменная является локальной для потока (а также, если она участвует в выражении threadprivate, firstprivate, private или lastprivate)
  • Если обращение к переменной производится в коде, который гарантированно выполняется только одним потоком (в параллельной секции директивы master или директивы single).

4. Неоправданно большое количество кода в критической секции

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

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

5. Слишком частое применение критических секций

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

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

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

Заключение

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

1. Отсутствие /openmp

При создании проекта нужно сразу же включить соответствующую опцию.

2. Отсутствие parallel

Необходимо тщательно следить за синтаксисом используемых директив.

3. Отсутствие omp

Необходимо тщательно следить за синтаксисом используемых директив.

4. Отсутствие for

Необходимо тщательно следить за синтаксисом используемых директив.

5. Ненужное распараллеливание

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

6. Неправильное применение ordered

Необходимо тщательно следить за синтаксисом используемых директив.

7. Переопределение количества потоков внутри параллельной секции

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

8. Попытка использовать блокировку без инциализации переменной

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

9. Попытка снять блокировку не из того потока, который ее установил

Мастер Йода рекомендует:  Как работают псевдоклассы в CSS. Подробное объяснение с примерами и диаграммами

Каждый из потоков, использующих блокировку, должен содержать вызов как блокирующей (omp_set_lock, omp_test_lock), так и разблокирующей (omp_unset_lock) функции.

10. Попытка использования блокировки как барьера

Каждый из потоков, использующих блокировку, должен содержать вызов как блокирующей (omp_set_lock, omp_test_lock), так и разблокирующей (omp_unset_lock) функции.

11. Зависимость поведения от количества потоков

Поведение вашего кода не должно зависеть от числа исполняющих его потоков.

12. Некорректное использование динамического создания потоков

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

13. Одновременное использование общего ресурса

Одновременный доступ к общему ресурсу должен быть защищен критической секцией или блокировкой.

14. Незащищенный доступ к общей памяти

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

15. Использование директивы flush с указателем

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

16. Отсутствие директивы flush

Отсутствие директивы flush может привести к чтению или записи некорректных данных.

17. Отсутствие синхронизации

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

18. Внешняя переменная задана как threadprivate не во всех модулях

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

19. Неинициализированные локальные переменные

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

20. Забытая директива threadprivate

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

21. Забытое выражение private

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

22. Некорректное распараллеливание работы с локальными переменными

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

23. Неосторожное применение lastprivate

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

24. Непредсказуемые значения threadprivate-переменных в начале параллельных секций

Значение переменной, объявленной как threadprivate, является непредсказуемым в начале параллельной секции, особенно если переменной до этого присваивалось какое-либо значение. Вообще от использования директивы threadprivate и выражений private, firstprivate, lastprivate лучше отказаться. Вместо этого рекомендуется объявлять локальные переменные в коде параллельной секции, а соответствующие начальные и конечные присваивания (если они необходимы) производить с общей переменной.

25. Некоторые ограничения локальных переменных

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

26. Локальные переменные не помечены как таковые

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

27. Параллельная работа с массивом без упорядочивания итераций

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

1. Ненужная директива flush

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

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

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

3. Ненужная защита памяти от одновременной записи

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

4. Неоправданно большое количество кода в критической секции

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

5. Слишком частое применение критических секций

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

Таблица 1 — Краткий список основных ошибок.

Вообще все ошибки можно разделить на три основные категории:

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

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

Как бы то ни было, все эти ошибки в большинстве случаев можно легко диагностировать автоматически, средствами статического анализатора. На данный момент диагностику некоторых (лишь очень немногих) из них выполняет Intel Thread Checker. Некоторые ошибки диагностируются компиляторами, отличными от компилятора Visual Studio. Однако специализированного инструмента пока не существует. В частности, Intel Thread Checker обнаруживает одновременный доступ к общим переменным, некорректное использование директивы ordered и отсутствие ключевого слова for в директиве #pragma omp parallel for [1].

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

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

Список использованных источников

  • Michael Suess, Claudia Leopold, Common Mistakes in OpenMP and How To Avoid Them — A Collection of Best Practices.
  • OpenMP Quick Reference Sheet.
  • OpenMP C and C++ Application Program Interface specification, version 2.0.

  • Yuan Lin, Common Mistakes in Using OpenMP 1: Incorrect Directive Format.
  • Richard Gerber, Advanced OpenMP Programming.
  • Yuan Lin, Common Mistakes in Using OpenMP 5: Assuming Non-existing Synchronization Before Entering Worksharing Construct.
  • MSDN Library article on ‘threadprivate’ OpenMP directive.
  • Andrey Karpov, Evgeniy Ryzhkov, Adaptation of the technology of the static code analyzer for developing parallel programs.
Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

Подводные камни парного программирования

Парное программирование в каком-то смысле напоминает парное патрулирование, когда у тебя есть комрад, который, наблюдая за ситуацией под другим углом, может прикрыть спину и спасти жизнь. На службе, как и в офисе, редко бывают лишними дополнительная пара глаз и толковая голова, о чем вовремя смекнули agile-евангелисты. И теперь двое за компом — это уже не тунеядцы, но адепты относительно нового движения, которое, с одной стороны, призвано увеличить качество разработки, но с другой — оставляет поле для вопросов. Когда включать парное программирование? Выгодно ли это? Нужно ли оно вообще?

Парное программирование — это когда за одним компьютером сидят два сотрудника: один кодит (driver), второй контролирует (observer). Они могут время от времени меняться местами. Kent Beck дает более простое определение: «All production code is written with two programmers at one machine».

В теории такая практика позволяет разработчикам добиться нескольких вещей:

1.Улучшить качество кода. Вторая пара глаз, если не спит, то вылавливает ошибки еще «теплыми». Раскрыть преступление по горячим следам легче и быстрее, чем после, поэтому в данном случае парное программирование позволяет здорово сэкономить время.

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

3.Перенять опыт. В первую очередь это касается джунов, которым важно научиться думать как опытный программист. В данном случае парное программирование — это, по сути, отношения ментор-ученик.

4.Получить знания. Даже для уверенного разработчика «въезд» в новый проект требует некоторого времени. Можно, конечно, долбить стену в одиночку или регулярно заваливать вопросами коллег, но продуктивней будет провести несколько pair-programming сессий, которые прольют свет на то, вокруг каких компонентов всё вращается, где понаставлены костыли и каких шкафов со скелетами лучше избегать.

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

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

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

Что следует учесть перед началом парного программирования:

— Больше человеко-часов на задачу. Для программиста это ничего не значит: солдат спит — служба идёт. Но для работодателя это возросшие траты на работу двух человек вместо одного (в среднем pair-programming обходится на 15% дороже). Тогда как результат в виде меньшего количества багов и более удачного дизайна, который в итоге может компенсировать увеличенное число затраченных человеко-часов и даже удешевить продукт, не всегда очевиден.

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

«Personality may be a valid predictor for long-term team performance. However, we found no strong indications that personality affects pair programming performance or pair gain in a consistent manner, especially when including predictors pertaining to expertise, task complexity, and country.»

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

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

— Различный уровень мастерства. Грубое деление разработчиков на новичков и экспертов дает три варианта комбинаций: эксперт — эксперт, эксперт — новичок, новичок — новичок.

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

Эксперт — новичок. Фактически, это отношения ментор-ученик, которые, тем не менее, будут полезны не только джуну, но и ментору. Новичок будет обучаться, задавая вопросы, а учитель будет обучаться, передавая опыт (лат. — docendo discimus — обучая, обучаюсь, — Сенека). Кроме того, джун будет задавать много «глупых» вопросов, подталкивая эксперта более креативно подходить к решению задач.

Новичок — новичок. Эта пара обычно не только работает намного быстрее и качественнее, чем одинокий новичок, но и эффективнее, чем даже пара эксперт-эксперт.

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

— Ограниченное время. Согласно исследованию, опубликованному в 2009 году в журнале IEEE Transactions on Software Engineering, оптимальное время для работы в паре — от 1.5 до 4 часов. Иначе наступает ментальное истощение. При этом пары следует регулярно тасовать.

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

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

Problems, officer?

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

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

10 ошибок новичков в дайвинге

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

Просто помните, что на первых погружениях НЕ НУЖНО:

1. …пренебрегать взаимной проверкой перед погружением.

Каждый раз дважды проверять свое снаряжение, и снаряжение напарника.. Такая нудятина, правда? Подумайте еще раз. Снова и снова статистика подтверждает — взаимная проверка перед погружением спасает жизни. Не ленитесь!

2. …оставлять волосы под маской.

Сдуть лезущие в глаза пряди — очень милый и эротичный жест. Но вряд ли у вас получится сделать это под водой. Удаление волос из-под маски помогает избежать постоянного ее заливания и связанного с ним дискомфорта.

3. …выбирать костюм абы как.

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

4. …находиться под водой в вертикальном положении.

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

5. …использовать руки.

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

6. …забывать побриться*

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

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

7. …пренебрегать неопреном.

Еще одна распространенная ошибка — слишком тонкий костюм. Помните, мы ныряем в холодную мокрую воду, даже если это происходит где-нибудь в тропиках. Дважды подумайте, перед тем как сказать себе «нет, я не замерзну!».

8. …не задавать вопросов.

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

9. …делать все сразу.

Фото под водой — это очень и очень здорово. Я показываю язык фотографу, бадди и хвост какой-то рыбки, коралл на фоне чьей-то ласты, спину с баллонами… Однако во время первых погружений у вас будет куча более важных задач, помимо борьбы с камерой. Сконцентрируйтесь на базовых навыках и только потом добавляйте «фан».

10. …пренебрегать проверкой плавучести.

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

Текст: Сергей Вдовенко, инструктор PADI

Рубрики: Самиздат

Вам понравилась статья? Поделитесь с другими!

Учимся программировать квантовый компьютер на основе игры «Морской бой»

«Квантовые компьютеры — довольно интересный новый вид вычислительной техники. Они словно мчатся через немыслимое количество параллельных вселенных, чтобы быстрее запускать программы. В их работе используются принципы, вводившие в заблуждение даже Эйнштейна. Это волшебные коробки, из-за которых ваши блоки с транзисторами будут храниться в пыли», — вероятно, такой текст вы увидите в популярных научных статьях. Они, несомненно, внесли свой вклад в то, чтобы новая технология звучала здорово. Однако они же, авторы подобных статей, могут превратить квантовые вычисления в какое-то оккультное искусство, заниматься которым позволено только самым умным учёным. Но несмотря на то, что это не так, большая часть программистов не увлекается квантовыми вычислениями.

Такие компании, как IBM и Google, занимаются созданием квантовых устройств. Это значит, что время игры с кубитами уже настало.

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

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

Примечание Обратите внимание, что код на Python, приведённый в статье, далёк от идеала и служит лишь для объяснения принципов работы квантового компьютера.

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

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

Что такое квантовый компьютер?

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

В основе работы обычного компьютера лежат биты — переменные, имеющие только два значения: 0 и 1. Хотя, в контексте булевой алгебры их также можно называть «true» и «false». Неважно, как мы их называем. Важно лишь то, что существует два состояния.

С помощью битов можно выполнить простые битовые операции, такие как побитовое отрицание (NOT), побитовое «И» (AND) и побитовое «ИЛИ» (OR). Они являются основой вычислений. С битами можно сделать всё что угодно. Любая более сложная переменная, чем бит ( int или float ) — это просто набор из множества битов. Любая более сложная операция, чем побитовое «И» или побитовое отрицание (NOT) — это просто несколько таких операций, соединённых вместе. Именно так и работает обычный компьютер на самом базовом уровне.

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

В каком-то смысле состояния суперпозиции представляют собой значения, существующие между экстремумами 0 и 1. Кубит можно представить в виде сферы, на которой 0 и 1 расположены на противоположных полюсах, а состояния суперпозиции — все остальные возможные точки на её поверхности.

Кроме 0 и 1 на изображении выше обозначена пара важных состояний суперпозиции. Одно из них называется u3(0.5*pi, 0,0) 0 . Не самое запоминающееся название, но вы узнаете, что оно означает, по ходу чтения статьи.

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

Важной отличительной чертой кубита является невозможность извлечения из него недвоичной информации. Законы физики по своей природе нам не позволяют узнать, что именно кубиты делают. Не получится спросить у кубита точные детали о положении его суперпозиции. Можно только заставить выбирать его между двумя противоположными точками на сфере (вроде 0 и 1). Если это что-то, что не является ни нулём, ни единицей, то кубиту просто нужно будет случайным образом решить, что выбрать.

Кубит имеет некоторые свойства непрерывной и дискретной переменных, но не является ни той, ни другой. Кубит — квантовая переменная.

Что предстоит делать с кубитами?

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

Мастер Йода рекомендует:  FAQ по MS SQL 7.0

Можно использовать кубит для каждого корабля, чтобы симулировать эти действия на квантовом компьютере. Ноль может означать невредимый корабль, а единица — полностью уничтоженный корабль. Состояния суперпозиции могут служить для отображения степени повреждений корабля. Корабль, уничтожаемый с одного выстрела, довольно просто симулировать. Можно его инициализировать с нулём, а затем применить побитовое отрицание (NOT), когда он будет уничтожен.

Давайте посмотрим, как реализовать этот простой процесс на квантовом компьютере. Мы не будем обращать внимание на какие-либо другие корабли на этом этапе.

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

Первые две линии всегда находятся в верхней части любого файла на QASM. После этого мы определяем все биты, которые нам понадобятся: кубиты и обычные биты. В этом примере мы ссылаемся на этот кубит в коде как q[0] . В примере мы определили один кубит в регистре под названием q . Так как на выходе должна быть нормальная удобная для чтения информация, мы также определяем один нормальный бит в регистре «c».

Кубит q[0] автоматически инициализируется в состоянии 0 , так как это то состояние, с которого мы хотим начать, дальнейшая подготовка не требуется.

Затем нужно взять q[0] = 0 — полностью целый корабль – и выполнить побитовое отрицание (NOT). С обычным битом и с обычным языком программирования это выглядело бы так: q[0] = !q[0] (для C++) или q[0] = not q[0] (для Python). В случае с QASM это может быть выполнено несколькими разными способами. Самый простой заключается в использовании операций с x и y . Ниже приведены некоторые примеры использования этих операций с эквивалентами на C++ и Python для сравнения.

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

Вы скорее всего заметите, что ни x , ни y не присутствует в предыдущем файле на QASM. Вместо них написано следующее: u3(pi, 0, 0) — ещё один способ применения побитового отрицания (NOT).

Это полностью равнозначно использованию x . Но u3 — более сложная операция. У неё есть три аргумента, изменяя которые, можно выполнять другие действия.


Первый аргумент — угол со значением, выраженным в радианах. Это тот угол, вокруг которого будет вращаться сфера нашего кубита. Угол pi равен 180°, это значит, что сфера будет перевёрнута полностью с ног на голову. 0 становится на место 1 , а 1 — на место 0 , поэтому эта операция и выглядит как побитовое отрицание (NOT). Чтобы выполнить побитовое отрицание наполовину, можно просто использовать половину угла: u3(0,5*pi,0,0)

Теперь появился ещё один способ представления побитового отрицания при работе с кубитом: можно выполнить половинное побитовое отрицание дважды.

Аналогичные действия можно сделать с 1/3 побитового отрицания, выполнив его трижды и так далее.

Последняя строка в файле на QASM выглядит так:

Таким образом измеряется кубит. Мы как бы говорим q[0] решить, чем быть: единицей или нулём. Конечный результат сохраняется в c[0] , и наш мозг или обычный компьютер уже смогут прочитать его. Значение c[0] заменяется результатом вычисления на выходе.

Если ничего не было сделано в период между инициализацией и измерением, то результат всегда должен быть таким: c[0] = 0

Если было выполнено побитовое отрицание, то результат всегда должен быть таким: c[0]=1

Если выполнили только половинное побитовое отрицание, то q[0] будет между 0 и 1 в процессе измерения — а именно в состоянии суперпозиции, к которому мы уже обращались раннее: u3(0.5*pi,0,0) 0 . Измерение принадлежности его к нулю или единице заставит его случайным образом выбрать одну из двух позиций с равной вероятностью.

Эта вероятностная природа придаёт квантовым вычислениям некую случайность. Дополнительная хаотичность также присутствует в современных устройствах из-за шумов. Хотя это и происходит на довольно низком уровне, не стоит об этом забывать. Именно из-за шума иногда получается 1 на выходе, вместо 0 и наоборот.

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

Создание игры

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

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

Гибридная природа устройства повлияет на способ написания кода. Использование QASM отлично подходит для программирования квантовой части устройства, а, например, Python — для стандартной, так как многие разработчики знакомы с этим языком программирования.

Это тот способ, который используется в текущей версии API IBM. И именно его мы используем в этой статье. Есть, конечно, другие подходы, например, Project Q.

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

Для запуска программы вам потребуется Jupyter, а также зависимости SDK.

Теперь давайте обратимся к коду.

Прежде всего нужно импортировать всё, что понадобится для запуска кода на IBM Quantum Experience.

Этот код ссылается на файл Qconfig , который понадобится для создания учётной записи на сайте IBM. Не бойтесь, это бесплатно. Обратитесь к руководству по SDK IBM для получения более подробной информации.

Далее следует несколько стандартных импортов и некоторая информация об игре, выводящаяся на экран.

Игроку задаётся вопрос, в котором ему нужно ответить, какое устройство следует использовать для запуска квантовой части схемы. Существует три основных варианта. Один из них заключается в симулировании квантовых файлов на вашем собственном компьютере ( local_qasm_simulator ). Ещё один способ основывается на запросе к IBM на выполнение симуляции на их компьютерах ( ibmqx_qasm_simulator ). Также можете использовать реальное квантовое устройство с 5 кубитами: ibmqx2 . Мы спрашиваем игрока, хочет ли он использовать квантовые устройства или симулировать выполнение кода локально. Этот процесс не настолько сложный, чтобы использовать мощности IBM для его выполнения.

На следующем этапе нужно добавить ещё одну важную переменную: количество запусков. Это нужно для сбора статистики.

Нет никаких определённых причин для использования числа 1024. Вы можете выбрать другое.

Теперь нужно перейти к настройке досок, на которых игрок будет выбирать позиции для трёх кораблей. Доступно всего 5 возможных позиций, соответствующих пяти кубитам на чипе IBM ibmqx2. Ниже представлена визуализация сетки.

Числа — имена, используемые для каждой позиции. Они также являются именами, которые использует IBM. Например, 2 — позиция кубита q[2] .

Выбор, сделанный игроками, хранится в shipPos , там же есть запись для каждого из двух игроков ( player=0 — для первого игрока и player=1 — для второго) и для каждого из трёх кораблей, которые они размещают. Запись shipPos[player][ship] хранит позицию ( 0 , 1 , 2 , 3 , 4 ) для корабля с номером, принадлежащим определённому игроку.

Получение выбора от игроков в основном осуществляется следующим образом:

Примечание Обратите внимание, что в статье приводятся блоки кода. Ссылка на полный код будет в конце статьи.

Этот код выводит следующее: «Player player , choose a position for ship ship+1 (0, 1, 2, 3 or 4)», а затем запрашивается ввод данных. Использование getpass не позволит другому игроку увидеть входные данные на своём экране. Заметьте, что первый корабль называется ship=0 , но для обычных людей счёт начинается с 1, собственно, поэтому ship=0 — корабль 1 для людей. Эти две строки встроены в пару циклов над «игроками» и «кораблями» для извлечения всех необходимых значений. Они также фактически не находятся рядом друг с другом в реальном коде, так как между ними располагается другой код, который гарантирует, что игрок ввёл корректное значение. Возможно, ваш способ реализации будет лучше, поэкспериментируйте.

Теперь пришло время для основного цикла игры. На этом этапе задаётся вопрос о выборе места для атаки поля оппонента. Затем результат добавляется в запись bomb[player][position] , которая подсчитывает, сколько раз игрок атаковал позицию в течение игры.

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

Если мы пишем файл напрямую, то начать следует так:

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

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

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

Здесь «gridScript» — то, что мы называем схемой в спецификациях. А gridScript — то, на что мы будем ссылаться при добавлении новых операций. Можно было бы использовать разные имена, но уже поздно.
Чтобы добавить операции над квантовыми и классическими битами, нужно определить, как мы будем ссылаться на них в коде.

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

Но при помощи замены frac и position на числа. Это можно сделать следующим образом на Python:

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

С точки зрения кубитов, это значит, что бомба, которая попадает по первому судну, применяет полное побитовое отрицание (NOT). Для бомбы, которая попадает по второму кораблю, применяется половинное побитовое отрицание, а при попадании по третьему кораблю применяется 1/3 побитового отрицания. Дробь frac определяется тем, какой корабль атакован. Реализуется таким образом: frac=1/(ship+1) .

После завершения бомбардировки наступает время просмотра результатов. Для этого нужно добавить команды измерения. Каждый кубит в регистре измеряется и результаты копируются в соответствующий стандартный бит. Так это реализуется:

Это создаёт строки кода на QASM:

В нашем распоряжении появился полноценный файл QASM, сохранённый как gridScript . Пришло время запустить его:

Здесь device и shots были определены ранее, когда мы попросили игрока выбрать устройство. Прогресс будет обновляться каждую секунду простоя, а затем перестанет обновляться после таймаута.

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

После выполнения задания можно извлечь данные:

Результаты копируются в grid вместе с результатами бомбардировки кораблей игрока ( player ) в grid[player] .

Результаты сохраняются в grid[player] в виде словаря. Ключами служат строки битов и поэтому выглядят примерно как 110 . Самый правый бит — c[0] , поэтому c[0]=0 в этой примерной строке битов. После обработки результатов мы можем определить, сколько запусков было у каждого результата (0 или 1) для каждого из обычных битов.

Мы интерпретируем дробь в виде процентного урона для корабля для тех моментов, когда кубит измерялся единицей. Нельзя ожидать 100%-го урона из-за влияния шумов (или погоды, если перенестись в нашу вселенную). Поэтому считаем, что урон превышает 95%, так как корабль утонул.
Давайте рассмотрим несколько примеров на выходе из квантового процесса. Предположим, что один игрок поставил свой третий корабль на позицию 2. Затем оппонент его бомбит. Результат будет примерно таким:

Здесь во время 290 запусков из 1024 было найдено 1 в позиции 2, показывая, что корабль действительно был повреждён.

Другой пример: один игрок поместил свой корабль на позицию 1, а второй — на позицию 2. Оппонент бомбил позицию 1 в первом раунде и позицию 2 во втором раунде. Результаты были бы такими:

Здесь все результаты имеют 1 во втором слоте справа (тот, который для позиции 1). Этот корабль был уничтожен одной бомбой. Второй на позиции 2 был повреждён только наполовину, поэтому половина результатов выдаётся с 1 для этого корабля.

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

Если произошла ошибка при извлечении файла QASM на квантовом чипе (или симуляторе), то на выходе вы увидите такое сообщение:

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

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

Игра продолжит запрашивать бомбардировки и запускать сценарий, пока один из игроков не потеряет все свои корабли. В этот момент побеждает другой игрок.

Вы могли заметить, что бомбардировка третьего корабля игрока даёт урон около 25%, поскольку этот корабль является третьим на пути к уничтожению. Вы, вероятно, ожидали, что урон будет составлять 33%. Но нет, тут дело в тригонометрии. Сейчас не будем на этом останавливаться.

Важно отметить, что сценарий повторяется каждый раз. Если время между очередями будет около одной минуты, но это почти вечность для кубитов. Даже при температуре в -273,15 градусов по Цельсию информация сгорела бы задолго до того, как пришла бы её очередь. Кроме того, наша потребность в извлечении информации также нарушит прекрасную квантовость в системе. По этим причинам любой процесс, который нуждается в человеческом взаимодействии, потребует, чтобы квантовые части повторялись с нуля для каждого нового ввода.

На этом статья подходит к концу. Морской бой теперь запускается на квантовом компьютере. Конечно, не самое необычное применение для квантового компьютера и не самая необычная версия морского боя, но всё-таки это интересно.

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

ТОП-10 самых легких для изучения языков программирования

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

WP Engine опросил 909 разработчиков в США. Наибольший процент из них (14%), что не удивительно, родом из Калифорнии. Большое количество программистов проживает в таких штатах как Флорида, Нью-Йорк и Техас. Около 70% всех опрошенных программистов были мужчинами, 30% — женщинами.

Перед вами 10 самых легких для изучения языков программирования с указанием процента проголосовавших за них разработчиков (согласно данным Wp Engine).

HTML (13,3%)

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

Python (9%)

Python известен своей высокой читабельностью и простым синтаксисом, что делает его легким для изучения. “Благодаря логичности и простоте Python легок в использовании и доступен, особенно для кодеров-новичков”, – говорится в исследовании. Созданный в 1989 году и увидевший свет в 1994, Python используется вот уже 25 лет.

Javascript (6,2%)

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

PHP (4,9%)

PHP – язык сценариев. Имеет открытый код и относится к языкам общего назначения. PHP особенно подходит для веб-расширений и может быть встроен в HTML.

Java (4,6%)

Это язык программирования общего назначения, специально предназначенный для максимального уменьшения зависимостей в реализации. Создан в 1991 году. Java также был назван наиболее благодатным и универсальным языком.

R это язык программирования с открытым кодом для статистических вычислений и работы с графикой. Он поддерживается организацией “Организацией статистических вычислений R”.

Shell (4,4%)

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

Ruby (4,1%)

Ruby это динамический язык программирования с открытым исходным кодом. Делает главный акцент на простоте. Также занимает высокие позиции в списке наиболее креативных языков.

Erlang (3,8%)

Erlang это многопоточный функциональный язык программирования общего назначения. Используется для разработки параллельных и распределенных систем.

Go (3,6%)

Go, язык программирования с открытым кодом, разработанный Google. Весьма полезен при создании веб-приложений с минимумом фреймворков, веб-серверов и API. Go app также легко запускать на облачной платформе Google. Этот язык был отмечен также как наиболее интуитивный.

Пять подводных камней для начинающего режиссера

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

Текст: Екатерина Афанасьева

Итак ты хочешь стать режиссером. Ты вдохновился гениальными фильмами Хичкока, Скорцезе, а может, тебя привлекает творчество Тарантино. Ты уже видишь режиссерское кресло с вышитым на нем своим именем. Ты стоишь на пьедестале, сжимая в руках вожделенный «Оскар», и произносишь пламенную речь. Ты уже чувствуешь мягкую жижу, в которую погружаешь свои руки на Аллее славы. Но потом реальность напоминает тебе о том, что.

1. Ты только в начале пути

Первое, что частенько подводит начинающих режиссеров, — они забывают о том, что они только в начале пути. И что до регалий и даже собственной страницы на «Кинопоиске» может пройти много времени. И что в отличие от Америки, в России никто не собирается в тебя вкладывать. Никто не встречает тебя с распростертыми объятиями и табличкой «Добро пожаловать в кино». И тебе придется упорно доказывать свое право называться профессионалом. Диплом факультета режиссуры не поможет отмахнуться от стай скептически настроенных по отношению к твоим мечтам знакомых, а купленная камера еще не сделает тебя режиссером. Нужно понимать, что на пути к «Оскару» возникнет несметное число трудностей, и их гораздо больше, чем на любом другом пути именно из-за того, что многие люди считают дорогу в кино прогулкой по лепесткам роз от собственного бунгало до лазурного моря. Но это, к великому моему и не только моему сожалению, не так. Единственное, чем можно утешить начинающего режиссера, — дальше будет легче. Как на скачках. Чем большее количество препятствий перепрыгнет твоя творческая лошадка, тем большее количество конкурентов ты оставишь позади себя. Но не все готовы ждать. И только острое, жгучее желание снимать кино поможет переплыть на ту сторону реки.

2. Ищите искусство в себе, а не себя в искусстве

Знаменитая фраза Станиславского, которую нужно написать на плакате и повесить на видном месте всем, кто пытается найти себя в творчестве. В последнее время слишком много людей, которые за счет поиска себя в искусстве пытаются решить личные проблемы. Совет — обращаться прежде всего к зрителю, а не к своему внутреннему ребенку. Когда ты начинаешь снимать кино, это большая ответственность. Уже с самого первого кадра, как только ты берешь в руки камеру, ты обращаешься к огромному количеству людей. Ты стоишь на сцене, глаза слепят софиты, ты не видишь зрителей. Ты не знаешь, что за Вася сидит в пятом ряду, но ты должен затронуть струны души и Васи из пятого ряда, и Мариванны с бельэтажа, просто потому что в этом сама суть искусства — оно направлено на развитие человека, на его улучшение. Не стоит приходить в кино и пытаться стать режиссером с заведомо заявкой на собственную «самость». Если ты рьяно держишься за свое «я» в сценарии, это показатель непрофессионализма. Самовыражение — это чудесно, но далеко от настоящей работы.

3. Экономика и продажи

Не хочешь продаваться? Ничего с этим не поделаешь. Потому что творчество — это продукт, а ты можешь считать себя производителем продукта. И у тебя есть зрители — покупатели. Законы продаж работают и здесь. И если ты считаешь, что можно налепить бессмысленных кадров под непонятную музыку, лишь бы «покрасивше», то попробуй продать синий хлеб. Возможно, это необычно и об этом поговорят, но в повседневной жизни неактуально. Не говорю, что нужно снимать для масс-маркета, но товары народного потребления — это не так уж и плохо. Особенно для начала.

4. Актеры второго плана могут разрушить ваш фильм

Можно иметь идеальную раскадровку, проработать эмоциональную арку сцены вдоль и поперек и выжать главных актеров, как лимоны, но достаточно прокола с мелкой фоновой деталью, и это может разрушить всю твою работу. Когда ищешь массовку, то понятия не имеешь, смогут ли они на самом деле выполнить то, что от них требуется. Обычно многие выбирают наиболее интересные лица, но это как сайт онлайн-знакомств: отобранные люди появляются на съемочной площадке, и в семи случаях из десяти невозможно убедиться, что это именно они. Фотографии с действительностью расходятся колоссально. Не говоря уже об актерском мастерстве. Когда слышишь про Джеймса Кэмерона, который подошел к каждому актеру из массовки на съемках «Титаника», чтобы дать ему имя, предысторию и цель, то начинаешь понимать, для чего он все это делал! Создание первого фильма для начинающих режиссеров сродни магии, и они не обращают внимания на детали, но именно детали и отличают профессионала от новичка.

5. Послеродовая депрессия

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

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

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

Добавить комментарий