понедельник, 27 января 2014 г.
§7. О терпении
пятница, 24 мая 2013 г.
§ 6. О LabVIEW программистах и их проблемах
Просматривая форумы по LabVIEW я всё больше замечаю некую профессиональную деградацию программистов. Вообще программирование в LabVIEW — оно чем-то похоже на магию. Вы соединяете квадратики друг с другом, и чудесным образом программа начинает получать данные, производить некие измерения, выводить графики на экран... Проблемы начинаются тогда, когда что-то перестаёт работать. Начинают раздаваться крики - Виндовс глючит! Лабвью глючит! Вчера всё работало, а сегодня - сломалось! В таком случае мне всегда хочется сказать, печально глядя на нынешнее поколение: компьютер — он ведь тупой до безобразия, там только биты да байты, он делает ровно столько, сколько от него требуют (я не буду брать в расчёт ошибки аппаратные - они редки, да и проявляют себя как правило совсем иначе). Конечно, техника усложняется, и сегодня прикладному программисту, разрабатывающему приложения на высокоуровневом языке, нет особого смысла наизусть знать — сколько регистров у процессора, особенности работы кэш памяти, и так далее. Однако иметь общее представление об архитектуре компьютера, особенностях операционной системы, принципах функционирования приложений и взимодействия их с операционной системой, иметь представление о работе с памятью, программист просто обязан. В этом смысле прежде чем начинать работать на высокоуровневом функциональном языке имеет смысл научиться программировать на языке классическом. Лучше всего начать с Паскаля, затем изучить Си (что будет совсем не лишним для LabVIEW - хотя бы для уверенного использования Formula Node) и лишь затем переходить на следующий уровень. Кроме того имеет смысл воспитать в себе некую "пытливость ума" - ведь у любой проблемы, будь то не найденный файл, или падение программы, всегда есть причина. Сам процесс поиска проблем тоже может оказаться увлекательным путешествием в мир логики. И разобравшись, вы обнаружите, что нет никакой магии, а есть только биты и байты.
понедельник, 28 июня 2010 г.
§ 5. О вызовах DLL из LabVIEW
- производитель какого-либо устройства предоставил вам DLL для коммуникации
- требуется использовать WinAPI, например для работы с окнами
- ускорение критичных участков (LabVIEW довольно медленна сама по себе, как правило переписывание части кода на Си позволяет ускорить выполнение в несколько раз)
Для упражнений нам помимо LabVIEW понадобится какой - либо компилятор, позволяющий скомпилировать код в DLL. Я пользуюсь компилятором CVI, также подойдёт VisualStudio (бесплатная Express в том числе). Я не буду описывать процесс создания DLL и пошаговое руководство по подключению DLL к LabVIEW, об этом вы можете прочитать на сайте NI, например в статье Using Existing C Code or a DLL in LabVIEW.
Итак, начнём. Прежде всего надо заметить, что существуют два типа связи сторонних DLL с LabVIEW - статическая линковка и динамический вызов. В первом случае вы явно указываете имя и путь DLL в настройках. Во втором случае вы указываете путь к DLL на блок-диаграмме:
В основном используется статическая связь. Однако вы не сможете запустить VI, если требуемая библиотека будет отсутствовать. В подобных случаях можно подгружать библиотеки динамически, при этом на загрузку библиотеки и поиск соответствующих функций потребуется некоторое время — вот маленькое исследование на эту тему Call Library Function Node Calling Optimization.
А вот с чем имеет смысл досконально разобраться, так это с соглашениями о вызовах. Вы, вероятно уже заметили, что при подключении внешнего кода требуется установить флажок "Calling conventions":
А чем, собственно, отличаются "stdcall (WINAPI)" и "С"? Существует несколько соглашений о вызовах (stdcall, cdecl, thiscall, fastcall). Нас сейчас интересуют первые два (thiscall используется в С++, а fastcall — довольно редко). Наиболее часто используется cdecl (этому соглашению соответствует опция C), а stdcall в основном используется фирмой Микрософт как основное соглашение для вызовов WinAPI. Как правило по умолчанию в компиляторах включён режим cdecl.
Давайте сделаем небольшую тестовую DLL, в которой исследуем то, каким образом передаются и возвращаются параметры, а также изучим различные соглашения о вызовах. Мы можем смело скомбинировать оба соглашения в рамках одной DLL, для этого напишем следующий код:
Параметры в DLL можно передавать по значению или по ссылке. В вышеприведённом примере параметр In передаётся по значению, а параметр Out передаётся по ссылке (то есть передаётся не значение переменной, а адрес, по которому она находится). Подключение библиотеки выполняется следующим образом:
Обратите внимание на строку, где показывается прототип вызываемой функции — используйте эту строку для контроля правильности описания параметров:
Что же происходит "за кадром"? Давайте заглянем внутрь наших функций:
Вот как выглядит функция, вызываемая как stdcall:
А вот как выглядит функция, декларированная как cdecl:
Итак, как мы видим, происходит следующее:
Параметры передаются через стек. Регистр esp - это и есть указатель на стек. Через стек передаются два аргумента. Параметр In смещён на четыре байта и его значение переносится из стека в регистр eax. Использование именно eax не случайно - это именно тот регистр, через который осуществляется возврат значения функцией (return In). Второй параметр Out смещён на восемь байт и его значение (при этом мы помним, что передали адрес) заносится в регистр edx. Третьей командой мы записываем значение eax по адресу, находящемуся в edx, затем возвращаемся в исходную программу. Вот здесь мы и видим отличие двух соглашений о вызовах. При вызове cdecl параметры заносятся в стек в обратном порядке, при этом указатель стека не изменяется (кстати, это даёт возможность вызывать функции с переменным количеством параметров, но не в LabVIEW). При вызове stdcall параметры передаются в прямом порядке, так как они объявлены в декларации, при этом указатель стека смещается на необходимое количество байт. В случае stdcall команда retn 8 выполняет коррекцию стека на 8 байт при выходе, а при использовании cdecl коррекция стека в теле функции не требуется. Что произойдёт в случае, если мы перепутаем соглашения? Мы получим неверный указатель стека при завешении функции, что в любом случае приведёт к проблемам. Дело даже не в возможной утечке памяти в стеке, а ещё и в том, что в стеке находятся другие данные, и неверный указатель практически моментально (или через некоторое время) приведёт к падению программы. Об этом также можно почитать в разделе помощи: Configuring the Call Library Function Node. И вот здесь мы натыкаемся на прелюбопытнейший эффект: программа продолжает работать как ни в чём не бывало даже в том случае, если мы неверно указали тип вызова! Оказывается, LabVIEW автоматически корректирует указатель стека даже в случае нашей ошибки. После нескольких экспериментов легко выяснить, что это происходит при установке флага Error Checking Level в Default (именно это значение и стоит по умолчанию):
Таким образом мы видим, что по всей вероятности LabVIEW сохраняет значение указателя стека перед вызовом функции, а затем проверяет значение после выхода из функции и выполняет автоматическую коррекцию в случае, если это необходимо (поскольку прототип вызываемой функции известен, а, следовательно известно и ожидаемое значение указателя стека после вызова). Заметьте, что никаких сообщений об ошибках при этом не выводится.
Что произойдет в случае выбора других опций? Это легко выяснить:
Disabled - приводит к немедленному падению LabVIEW, что и следовало ожидать
Maximum - приводит к ошибке 1517:
Сообщение об ошибке в случае неверного типа функции документировано, об этом можно почитать в статье Call Library Function Dialog Box. Таким образом следует обращать внимание на правильность указания соглашения о вызове. В случае неверного указания типа ошибку можно заметить не сразу, а например при изменении опции контроля ошибок.
Ещё один вопрос, на который мы можем ответить — что произойдёт в том случае, если мы укажем, например, то, что фукнция возвращает значение, хотя на самом деле тип функции void, либо наоборот. В данном случае фатальных последствий не будет, так как возвращаемое значение просто записывается в регистр eax, так что в одном случае мы просто получим значение этого регистра, а в другом случае просто проигнорируем его. Утечек памяти и исключений не возникнет (пытливый читатель может разобраться с тем, что происходит при возврате восьмибайтовых типов или строки самостоятельно).
Ещё одна из самых типичных ошибок — это неверное указание типа передаваемых параметров. В вышеприведённом примере наша функция имеет два параметра. Один из них (тот, который Out) передаётся по ссылке, и мы должны указать, что передаётся адрес переменной. Давайте намеренно совершим ошибку и передадим нулевое значение вместо адреса:
Запись по нулевому адресу немедленно вызывает исключение, а вот внешнее проявление опять-таки зависит от опции Error Checking. В случае Maximum или Default результатом будет ошибка 1097:
В случае, если мы выключим обработку ошибок вообще, то LabVIEW "упадёт". Таким образом, включение обработчика ошибок приводит также и к включению внутреннего обработчика исключений (примерно также, как это делается в C++ секциями try... catch).
Побаловавшись с простыми типами, можно перейти к массивам. В простейшем случае мы просто передаём в функцию указатель на массив. Давайте попробуем сделать простое поэлементное копирование массивов:
Здесь важно понимать то, что функция понятия не имеет о размере передаваемых массивов, так что количество копируемых элементов надо явно указать как параметр и передавать извне. Кроме того надо обратить внимание на то, что память под массив надо резервировать снаружи, в LabVIEW:
Типичная ошибка, которую можно совершить — выход за пределы массива. Как правило это приводит к исключению, либо нарушению работоспособности программы. Здесь снова может помочь включение режима отлова ошибок на максимум - в этом случае LabVIEW сообщить вам то, что вы выскочили за пределы массива (впрочем это не всегда спасает её от падения). Возникает логичный вопрос — а нельзя ли передать одновременно с массивом и количество элементов, а также зарезервировать память внутри DLL? Это сделать можно, но перед рассмотрением этого вопроса имеет смысл поэкспериментировать с простыми структурами, так как у LabVIEW есть одна небольшая тонкость.
Сделаем следующий пример:

В лучшем случае результат далёк от ожидаемого, а в худшем код вызывает исключение.
Как же правильно передать структуру (читай - кластер) в DLL и обратно? Для этого нужно разобраться с тем, как хранятся данные в кластерах LabVIEW и в структурах Си. Основной документ, с которого имеет смысл начать — How LabVIEW Stores Data in Memory.
При передаче кластера в DLL мы фактически передаём указатель на адрес памяти, в котором хранится наш кластер. DLL принимает этот адрес и обращается к элементам структуры. Очевидно то, что реальное размешение кластера в памяти должно с точностью до байта отвечать ожидаемому расположению элементов в структуре.
Прежде всего надо заметить, что массивы, строки и пути не хранятся непосредственно в кластере LabVIEW. Вместо них хранятся адреса (причём не на непосредственно данные, а на заголовки). Таким образом в приведённом примере передача массива или строки в составе кластера смысла не имеет. Поскольку массив в нашем примере хранится непосредственно в структуре, то и передавать его надо поэлементно. Здесь нас подстерегает ещё одна опасность. Каков размер структуры в Си (тот, что возвращает sizeof(TD_MyStruct))? Если вы думаете, что 13 байт (int + char + 2 x int = 4 + 1 + 2 x 4), то вы ошибётесь. Дело в том, что между членами char и int "вставлено" три дополнительных байта, которых вы не видите. Это сделано для исключения пенальти при обращении по адресам, невыровненным на границу 32х бит. Примеры выравнивания в структурах можно посмотреть в msdn: Structure Alignment Examples. По умолчанию установлено выравнивание на границу 4 байт, так что истинный размер структуры в нашем примере не 13, а 16 байт. А вот в LabVIEW элементы кластеров хранятся без выравнивания. Чтобы решить проблему выравнивания, существуют два пути - либо вставить недостающие "padding" элементы в кластер (это может быть использовано в том случае, если DLL не может быть перекомпилирована):
либо явно указать компилятору что наша структура не требует выравнивания (это достигается при помощи #pragma pack(1) непосредственно перед объявлением структуры). Обратите внимание, что #pragma pack(1) будет распространяться на все структуры до момента пока не встретится следующая #pragma pack.
В заключение - маленький совет. Поскольку порядок следования элементов в кластере весьма важен, то для того, чтобы не проверять каждый раз порядок через меню "Reorder Elements in Cluster" просто установите опцию Arrange Vertically (доступно в контекстном меню) — в этом случае расположение элементов в кластере всегда будет отражать реальный порядок следования.
Вот собственно и всё, на что стоит обратить внимание при подключении стороннего кода из DLL в LabVIEW:
- соглашение о вызовах (stdcall или cdecl)
- передача параметров по ссылке или по значению
- выравнивание в структурах и порядок следования элементов.
За кадром пока что остались опции управления потоками исполнения (UI или любой поток), функции управления менеджером памяти LabVIEW из стороннего кода и функции обратного вызова при определённых событиях, но об этом как-нибудь в другой раз...
вторник, 2 февраля 2010 г.
§ 4. LabVIEW против С или гонка за лидером
Сегодня ещё несколько слов о быстродействии LabVIEW программ. В предыдущем параграфе было показано то, что генерируемый LabVIEW код довольно "рыхлый". Как это отражается на быстродействии и есть ли резервы? Давайте разберёмся.
В качестве простенького примера займёмся вычислением центра масс восьмибитной картинки. (если вы забыли, что такое центр масс или центроид, то загляните в Википедию: Centroid, Center of Mass) Для теста возьмём картинку Лены в четверть мегапиксела. В качестве референсного примера я воспользуюсь IMAQ Centroid из библиотеки IMAQ Vision:
Этот пример даст мне значение центра масс в точке (265.19, 247.43):
Классическая схема вычисления примерно такая:
Заметьте, что использование типа I64 в данном случае более чем оправдано, иначе возникнет переполнение.
Сколько времени займёт вычисление? Приготовим простенький бенчмарк:
На моём компьютере получается 8 миллисекунд. Неплохо.
Пример хорош тем, что даёт богатую пищу для размышлений. Что произойдёт, если перейти от целочисленого типа к DBL? Давайте проверим:
Получилось 5.5 миллисекунд. Уже лучше.
Немного потеряв в точности, можно перейти к типу SGL:
Теперь 3,75 миллисекунды.
Хороший программист сразу предложит вариант улучшения, позволяющий избавиться от вложенного цикла, например такой:
Теперь получилось 3,25 миллисекунды. Что ж, на этом, пожалуй, остановимся. Строго говоря, можно попытаться совсем избавиться от арифметики с плавающей точкой и перейти к целочисленному типу, оставаясь при этом в рамках 32 бит, но это вызовет значительное усложнение кода (а выигрыш будет невелик).
Давайте кардинально изменим подход и перепишем этот участок кода на чистом Си. К вопросу подойдём "в лоб", взяв за основу самый первый пример:
Это замечательное упражнение также помогает попрактиковаться в передаче массива и кластера в DLL:
Технически я пользуюсь средой CVI, на которую навешан Интеловский компилятор.
Что же получается?
0,84 миллисекунды! Наш код работает вчетверо быстрее аналогичного LabVIEW кода, что и требовалось доказать. (Пытливый читатель может сравнить ассемблерные листинги обоих примеров).
Достигли ли мы предела производительности? Конечно нет, ведь можно использовать факт наличия двух или более ядер (наш код пока что однопоточный), или использовать SIMD комманды. Однако несмотря на очевидный выигрыш, конечно, не стоит бросаться и тут же переписывать куски кода на Си. Отлаживать такой код на порядок сложнее, да и при современном быстродействии компьютеров как правило в этом нет нужды. Рассматривайте эту возможность как резерв, который всегда есть, и который можно и нужно использовать при необходимости. А «ранняя оптимизация» — она, как правило, вредна и порой приводит к неоправданному увеличению сложности. Сначала закончите и отладьте функциональность кода, затем определите «узкие места», попытайтесь оптимизировать LabVIEW код, и лишь потом используйте «тяжёлую артиллерию».
До следующих встреч,
Андрей.
CentroidLVvsC.zip (5MB)
четверг, 28 января 2010 г.
§ 3. Интерпретатор или компилятор?




- Classic Code.vi.zip (5.2 K)
- Code Extractor.vi.zip (13.4 K)
пятница, 22 января 2010 г.
§ 2. Об автоматической обработке ошибок
Суть автоматической обработки ошибок заключается в том, что при возникновении ошибки на выходе VI исполнение программы может быть остановлено (за диалог отвечает как раз вторая опция):
В вышеприведённом примере мы создаём новую папку, но такая папка уже существует, что и вызывает ошибку 10 (если у вас нет папки Windows на диске с:\ просто запустите этот пример дважды). Поскольку выход примитива CreateFolder не подсоединён, то возникающая ошибка вызывает прерывание выполнения программы.
В следующем примере это дилоговое окно не появится:
В ряде случаев подобные сообщения мешают. Например в вышеприведённом примере теоретически надо проверять код возврата для того, чтобы выяснить причину ошибки — либо папка уже существует, либо её создание невозможно по каким-либо причинам (например, недопустимая буква диска, либо нет прав на создание, либо недопустимые символы в имени и т.п.). При "первых набросках" или прототипировании программы проще отключить мешающие сообщения и не проверять ошибки типа описанной выше (большинство программистов — оптимисты и считают, что после вызова CreateFolder требуемая папка непременно будет создана если её ещё не существует).
Однако на заключительном этапе имеет смысл включить эту опцию и вычистить участки кода, вызывающие "оборванные" ошибки (ну или как минимум обратить внимание на такие места).
Важно также не лениться (вы, конечно, можете просто соединить выход с блишайшей границей цикла или последовательности). Если вы не готовы принять решение о логике обработки какой-то конкретной ошибки немедленно, то лучше сделать SubVI со входом ошибки и подосединить его к выходу ошибки в "проблемном" месте. Внутри этого SubVI вы можете организовать запись в лог-файл или в лог отладки (более детально мы это рассмотрим в другом параграфе). Таким образом вы всегда сможете найти места в программе, где проблема пока не устранена (кстати, это будет работать и в исполняемом приложении).
§ 1. Что отличает профессионала от любителя
Единственно верное написание — «LabVIEW». Только так и никак иначе.
Тем не менее, даже если вы пишете «LabVIEW» правильно, то это ещё не делает вас профессионалом.