Расшифровка семинара Александра Сморкалова (Xperience AI), который состоялся в мае 2020 года. Видеоверсия доступна для просмотра на нашем youtube-канале.
Сегодня я расскажу про инструменты, которые помогут вам в тестировании. Сначала поговорим про оборудование – где и что посмотреть про железо. Далее обсудим, как устроены современные динамические библиотеки на C, C++, поговорим про нативный код. Потом я расскажу про CUDA, OpenCL и прочие инструменты аппаратных ускорителей, библиотеки и все, что вокруг них. CUDA – самое интересное в этой части, потому что это то, чем сейчас пользуется большинство. Кроме этого, я дам пару полезных ссылок – куда сходить, что почитать. Попутно я покажу на практике, как этим воспользоваться и что надо поставить для того, чтобы все работало.
Диагностика оборудования и драйверов
Проблема номер ноль для многих, кто использует NVidia, CUDA, GPU и подобное – при очередном обновлении системы с оборудованием происходит что-то не то. Если вы используете Movidius Stick, PCI Express, FPG, еще что-то, встает вопрос: как ориентироваться в системе, как работает инструмент, все ли подключилось.
Сначала смотрим на процессор. Есть виртуальный файл /proc/cpuinfo, в нем все написано, от этого можно отталкиваться. В некоторых дистрибутивах Linux можно найти файл /proc/config.gz – там есть информация о том, как собрано ядро. В современной Ubuntu его нет, это опциональный файл, но на других системах он может присутствовать – и может быть полезен. Информацию про материнскую плату можно посмотреть с помощью dmidecode – она показывает все, вплоть до слотов памяти, версии BIOS, all-level настроек. Когда речь идет про перформанс, тестирование – эта штука пригодится. Далее есть список всяких ls утилит, которые показывают устройства на определенной шине. lshw – про все оборудование, lscpi – про PCI-Ex, lsusb – про usb и так далее. Если что-то пошло не так или устройство не обнаружилось так, как вы хотели, то все логи есть в dmesg. Там вы найдете все – или по крайней мере заметку на то, что смотреть дальше.
Дескрипторы, сокеты, системные объекты
Первая утилита – это lsof. Она показывает все открытые дескрипторы для одного процесса или всех, например, кто открыл данный файл или девайс. Если указать просто lsof <имя файла>, то там будет написано, кто открыл этот файл. В той ситуации, если у вас не монтируется флешка или файл по какой-то причине занят, lsof скажет вам, кто виноват. Если указать lsof -p <номер процесса>, он покажет все файлы, открытые конкретным процессом. Туда попадают не только файлы данных, но и все динамические библиотеки и некоторые другие псевдофайлы – все, что можно открыть через open. Аналогичная утилита про сетевую статистику – Netstat. Она говорит нам про то, кто какие сокеты или порты открыл, слушает, пишет и так далее. Это полезная штука, если вы запускаете какие-то сервисы, тот же Tensorboard, а ваш порт занят, но кем он занят – непонятно, это можно быстро узнать. Аналогичные инструменты есть и на Windows. На Windows есть набор утилит от бывшей компании Sysinternals Марка Руссиновича. Раньше это был развал отдельных программок, сейчас это большой кухонный комбайн ProcessExplorer. На Windows аналогично lsof можно посмотреть, какая программа какой файл читает, получить лог в реальном времени. То же самое касается и реестра, и логов драйверов ядра. Многие драйверы пишут лог в отладочную консоль, эту консоль нигде не видно, а DebugView ее показывает.
Мониторинг
Когда речь идет о вопросах, связанных с утечкой памяти, перформансом, какими-то еще проблемами деградации, полезно посмотреть на общее состояние – что происходит с процессами на вашей системе. Для этого есть три утилиты. Утилит вида top много, на всякий случай. Сегодня я бы хотел рассказать подробно про htop. Это самая полезная в нашем деле утилита для первоначального поиска проблем. Htop показывает загрузку процессора разными цветами. Зеленым цветом отмечены обычные потоки с обычным приоритетом в user space, красным помечается нагрузка процессорных ядер в режиме ядра.
Htop также умеет показывать процессы с низким приоритетом и процессорное время, которое отведено виртуальным машинам и системам виртуализации. Если при запуске вашей программы вы видите много красных палочек в загрузке процессора, это значит, что программа проводит больше всего времени на нижнем уровне, в ядре, и проблема связана с вводом-выводом, работой с устройствами и так далее. Аналогичная цветовая раскраска есть по загрузке памяти. Обычная память – это зеленый цвет. Синий цвет – кусочки памяти, которые отведены для промежуточных внутренних буферов. Оранжевым показывается кэш – в первую очередь, дисковый кэш. Linux использует всю свободную доступную память как дисковый кэш, поэтому когда вы делаете у себя локацию, то он выбрасывает часть кэша и отдает кусок памяти. Если ваш процесс имеет интенсивный ввод-вывод, но сам потребляет памяти немного, то бо́льшая часть памяти используется под кэш. Это очень хорошо.
В Unix и в Linux в частности процессы устроены иерархически. Дерево можно отстроить в htop, нажав на T. В общем дереве или списке с помощью F4 можно найти процесс, который вас интересует. С точки зрения отладки и поиска всяких проблем и неприятностей есть две полезные опции. Если выбрать какой-то из процессов и нажать E, то htop покажет переменное окружение, с которыми он был запущен. Это касается тестов, скриптов – можно посмотреть, что происходит не так, если вдруг поведение не такое, как вы ожидали. Аналогично lsof, можно выбрать процесс, нажать L и он покажет список открытых дескрипторов – файлов, в первую очередь. В частности, если ваш процесс или код на питоне подгружает модуль на ходу (например, OpenCV), то невозможно запустить какую-то утилиту и посмотреть заранее, как она загрузится. На работающем процессе можно посмотреть, какой же модуль загрузился, по списку дескрипторов и сделать выводы.
Библиотеки и символы
На всех популярных современных ОС – что Windows, что Linux, что MacOS, – код распространяется по большей части в виде библиотек и приложений, которые эти библиотеки используют. Практически всегда ваша программа зависит от какого-то количества библиотек. Это связывание происходит в run-time, когда вы запускаете программу. За связывание программы с библиотекой отвечает ОС. Она строит дерево зависимостей, обходит его и находит все связи. У каждой библиотеки или исполняемого файла есть таблица импорта/экспорта. Windows файл .exe может работать и как исполняемый файл, и как dll библиотека. В таблицу попадают функции, методы классов, константы, переменные и количество сущностей, которые вы не видите, не программируете напрямую. Например, если у вас в плюсовом коде есть виртуальные методы в классах, значит, компилятор вам генерирует VTBL, таблицу виртуальных методов, и эта таблица тоже становится неявной единицей экспорта, поэтому она попадает в таблицу экспорта. Если говорить про C, то там название функции, идентификатор переменной является уникальным. В C++ это не так, поэтому для того чтобы экспортировать символ наружу, в качестве имени функции используется не просто имя, но и дописанные к нему в виде префиксов все namespaces, имена классов и дописанный к функции список с типами параметров. Только так можно добиться уникальности функции или переменной. Имейте в виду, что для C++ кода, чтобы получить представление символа в нормальном плюсовом читаемом виде, имя нужно декодировать.
Кроме таблицы импорта/экспорта, в библиотеках есть набор дополнительных атрибутов. На разных системах он немного разный. На Linux у библиотеки есть так называемый SONAME. Это имя библиотеки как модуля, сущности, а не файла. RPATH – это набор путей, где искать зависимость. В частности, если вы собирали и запускали тест в OpenCV, исполняемый файл – testopencv.core. Он сам находит библиотеку opencv.core, несмотря на то, что OpenCV лежит не в системных путях. Это связывание происходит через RPATH. И в библиотеках, и в файлах есть список SONAME библиотек, которые являются нашими зависимостями. Важный момент – все функции, которые экспортируются из библиотеки, написаны текстом и их можно найти. Если у вашей библиотеки нет исходного кода, то отладчики используют таблицу импорта/экспорта для того, чтобы установить адреса какого-то хотя бы количества символов. То, что есть всегда.
Отладочные символы
Кроме таблиц импорта/экспорта, компилятор может добавлять в исполняемый файл отладочную информацию. В отладочных символах нет самого кода программы. Это только привязка к существующим файлам, поэтому работать с отладочными символами полноценно можно только с кодом в руках. Но даже если у вас нет кода, отладочные символы можно раскрасить, подписать все функции, переменные – практически все, что не оптимизировал компилятор. В ELF в исполняемых файлах на Linux отладочные символы – это отдельная секция внутри исполняемого файла или библиотеки. На Windows с Visual Studio это отдельный файл .pdb. На Linux есть утилита strip, которая может исключить эту секцию с отладочными символами и либо этот отрезанный кусочек, либо .exe библиотеку целиком, можно распространять отдельно. В частности, на Ubuntu и на Debian есть для многих пакетов соответствующий -dbg пакет, который включает в себя отладочные символы.
Инструменты
Давайте перейдем к тому, чем можно воспользоваться и посмотреть, кто от кого зависит, кто какие символы импортирует или экспортирует. Во-первых, утилита file, которая просто пишет тип файла. Она сама умеет определять исполняемые файлы. Если это ELF, executable или shared library – она об этом явно напишет. Что важно – ELF может написать вам архитектуру, под которую написан исполняемый файл. Он возьмет его из заголовка исполняемого файла. Кроме этого, она дает информацию о том, есть отладочные символы или нет. ELF напишет stripped или not stripped – то есть с символами или без символов.
Далее есть утилита ldd. Я думаю, что все про нее знают и пользовались. Она зовет линкер, пытается загрузить все динамические библиотеки для исполняемого файла, строит дерево и прописывает пути, адреса. Нюанс в том, что ldd никак не приклеишь к питону. Второй нюанс – ldd делает это статически при старте программы, поэтому если ваша программа загружает библиотеку через DLOpen, то ldd вам про это ничего не скажет. И тогда вам понадобятся lsof или htop, чтобы это выследить и понять.
Кроме этого, существует утилита readelf. Это парсер ELF формата. В частности, с помощью readelf -d можно посмотреть список зависимостей, SONAME, RPATH и прочую общую информацию про исполняемый файл или библиотеку. Утилита nm показывает список символов импортируемых и экспортируемых с их типом и с помощью нее можно получить информацию о том, какие присутствуют символы, сколько они занимают в памяти, какого они типа и так далее. И c++filt – маленькая утилита командной строки, которая поставляется вместе с JCC или с чем-то из набора для разработчиков. Это программа для декодирования закодированных C++ имен функций и других идентификаторов. Чуть позже, когда дойдем до практической части, я покажу, как этим воспользоваться.
На Windows есть утилита DependencyWalker. Это UI утилита, которая строит дерево динамических библиотек. Она повторяет работу системного линкера и с ее помощью можно понять, какие символы откуда экспортируются, какая библиотека подгружается в этот момент, где она лежит и так далее. Это такая замена ldd, readelf, nm. Для чего это нужно – чаще всего в разработке плюсового кода возникает ситуация, когда программы не линкуются в run-time по какой-то причине, не находят какую-то библиотеку, не хватает какого-то символа или еще что-то. Эти утилиты нам помогут эту проблему быстро решить. Кроме этого, nm умеет вводить список переменных, которые присутствуют в программе как символы и размеры которых он знает заранее. Их можно отсортировать, посмотреть, кто занимает больше всего памяти и на этом строить свою оптимизацию памяти. Я так делал при работе над контроллерами, очень полезная штука.
Трассировка
Несколько слов об утилитах, с помощью которых можно сделать трассу – посмотреть, что происходит с точки зрения потребления памяти, локации памяти, потребления ресурсов, вызова функций и так далее. Часть первая – это трассировка системных вызовов. В Linux с очень давних времен есть утилита strace. Она трассирует все вызовы к ядру и там видно все открытые и закрытые файлы, загруженные библиотеки, да и вообще все, что происходит с ядром. Аналогично strace, существует утилита ltrace, которая строит трассу, список вызовов к динамическим библиотекам. Например, мы используем OpenGL для рендера. Как понять, какие функции вызывались, в каком порядке и с какими порядками? Это можно делать через ltrace. Нас интересуют вызовы к конкретной библиотеке, так можно посмотреть, какие функции вызывались, а какие нет, с какими параметрами и как это дело проанализировать. Периодически такое бывает нужно и полезно.
Благодаря проекту valgrind все знают о memcheck, который долго и мучительно проверяет утечки памяти, нелицензированные переменные и прочие косяки работы программы с памятью. Но кроме этого, есть пара очень полезных штук. Во-первых, есть callgrind. Она может построить вам граф вызовов и сказать что-нибудь про ветвления, насколько они были предсказаны процессором и так далее. Но наиболее интересная утилита – это massif. Это инструмент внутри valgrind, который следит за локацией памяти. Во-первых, она собирает трейс о выделенных ресурсах и их источниках, а самое главное – с помощью нее можно построить профиль и посмотреть, сколько памяти в какой момент времени было выделено, понять пиковое потребление памяти и так далее. Что полезно для большинства читателей – эта штука работает приемлемо с питоном. Для утилиты massif есть визуализатор ms_print, который в консоли печатает картинку. И massif_visualiser, UI утилита, в которой все можно посмотреть.
Кроме прочего, для того, чтобы построить трассу и понять, как работает ваша программа, можно и нужно пользоваться всякими встроенными во фреймворки инструментами. Во-первых, это GStreamer. Его я буду упоминать много раз. Например, GStreamer имеет набор переменных окружения, которые управляют уровнем логгирования; он умеет писать описание графа пайплайна в виде .dot файла. Кроме этого, большинство фреймворков для оптимизации инструментов предоставляют свои возможности, библиотеки логгирования и библиотеки для того, чтобы строить трассы вызовов. В частности, подобные решения есть у NVidia, Intel ITT, насколько я знаю, интегрирован в OpenCV – им можно воспользоваться и посмотреть, кто сколько раз вызвался, сколько времени это занимало и так далее.
Фейки
Говоря про компьютерное зрение и проекты такого рода, имеет смысл упомянуть средства, которые позволяют зафейкать оборудование. Хорошая штука, которой я уже несколько раз пользовался – это V4l2loopback драйвер. Этот проект эмулирует V4l2 камеру, обычную веб-камеру. Эта штука есть в Ubuntu, как стандартный пакет просто из основного репозитория. Если вы хотите протестировать конечное приложение или у вас есть программы, входом которой вы не можете нормально управлять, можно подсунуть в чужое приложение любой ролик, который вы хотите, и посмотреть, как оно себя ведет.
Аналогичным образом, если мы говорим про IP камеры, можно использовать FFmpeg и Gstreamer. Они умеют организовывать RTP/RTSP сервер. Таким образом, можно сделать видеостриминг не с какой-то IP камеры, которую вам должен прислать вендор, а организовать все локально и быстро, что-то попробовать, поэкспериментировать и иметь результат, пока к вам едет оборудование. Еще интересная особенность, которая касается GStreamer и OpenCV: GStreamer имеет свой язык описания пайплайнов, с помощью которого можно описывать любую цепочку плагинов с их параметрами, построить любой пайплайн и запустить его с командной строки с помощью утилиты gst-launch. Этот же пайплайн в виде строчного описания можно подать в OpenCV. Главное - выбрать GStreamer как бек-энд для VideoCapture и тогда OpenCV отстроит этот пайплайн с GStreamerом. В VideoCapture в качестве параметра нужно указать строку с пайплайном вместо имени файла. Последний элемент в списке appsink – это выход для приложений. OpenCV найдет этот appsink и будет с него читать. Это полезная штука для экспериментов. Если вы знаете, что программа использует GStreamer с OpenCV, можно подсунуть туда свой фейковый видеоролик или какой-то странный пайплайн. А самое важное, что с помощью этого можно воспользоваться какими-то нестандартными плагинами в GStreamer для аппаратного декодирования видео. Кроме этого, обратите внимание на проект Xfvb – это X-server, который ничего не рисует на экране. Его полезно использовать для тестирования. Например, если вы хотите запускать ваше приложение для целей автоматизации и оно требует X, но вы хотите сделать в CI, где настоящих X у вас нет.
CUDA
Большинство из нас так или иначе CUDA использует, в первую очередь, для deep learning. Для начала пару слов об архитектуре. В большинстве случаев мы ставим CUDA на Ubuntu в виде deb пакетов. С левой стороны показана общая диаграмма по архитектуре CUDA, справа – имена пакетов, соответствующих этому уровню в архитектуре. Снизу у нас железо GPU, в ядре стоит модуль ядра, который обслуживает аппаратную часть, пакет называется nvidia-driver-версиядрайвера. Ему в пару ставится cuda1- и продолжение должно быть такое же, как у драйвера. Это так называемые драйверы API, это библиотека в пользовательском пространстве, которая обеспечивает взаимодействие с драйвером. Поверх нее строится API, который называется CUDA Runtime API, это библиотека lib-cuda-rt. Все функции, которые видны из lib-cuda1, начинаются с cu- и всегда имеют контекст в качестве параметра, управление происходит контекстом. cuda-runtime скрывает от нас контекст и все функции начинаются с cuda-. Например, есть cuda-alloc и cuda-malloc. Это вызовы с разных уровней. Поверх cuda-rt работают cuda-fft-, cuda-cublas- и все остальные библиотеки прикладного уровня. Поверх них обычно сидит какое-то приложение, это может быть OpenCV, PyTorch, что-то еще, что вам полезно.
Диагностика CUDA
Как нам, например, посмотреть, что происходит с железом NVidia? Для этого есть консольная утилита Nvidia-smi, которая ставится вместе с драйвером. Она показывает список GPU и их нагрузку. Там же есть температура и все остальное, но с точки зрения разработчика это не самая интересная вещь. NVidia в своем пакете с сэмплами предлагает в папке utilities два сэмпла, которые называются deviceQuery и drvDeviceQuery. Это две маленькие утилиты, которые опрашивают run-time API и драйвер API и печатают на экран все, что доступно в качестве информации из драйвера. Там пишется версия CUDA, список фич, размер памяти, Compute Capability и прочая информация. Например, если у вас поставлен NVidia драйвер для графики и все хорошо работает, NVidia сама отрепортит вам ваши параметры карты. Но если драйвер не подходит или CUDA run-time неполноценный, NVidia сама ничего не скажет. deviceQuery скажет вам по этому поводу все, даже если есть несовместимости. Другой момент связан с загрузкой модулей ядра. Есть открытый драйвер MView, который часть линуксового ядра. Если он у вас загружен вместо NVidia драйвера, то вы это там в Lsmod увидите.
Сборки CUDA
Нужно сказать пару слов про то, как собирать и запускать программы с CUDA, потому что есть нюансы. У NVidia есть такое понятие, как Compute Capability (CC). Это, грубо говоря поколение, версия GPU. С каждым выходом нового поколения GPU NVidia увеличивает СС. Ядра компилируются под конкретную архитектуру, но в пределах старшей версии поколения поддерживается обратная совместимость. В частности, в Kepler GPU есть версия с CC 3.0, а есть с 3.5. Если вы собрались под 3.0, то на 3.5 будет в целом все работать, но какие-то функциональные возможности работать не будут.
Кроме того, что можно собрать ядро под конкретное GPU, есть возможность собрать его в формате .ptx. .ptx – это промежуточное представление, которое потом компилятором дособирается под вашу конкретную GPM. С точки зрения C++ и системной части все, что собирает nvcc компилятор, собирается в виде объектного файла, потом линкуется в вашу динамическую библиотеку или исполняемый файл, и выглядит, как массив данных. Он имеет символ с точки зрения отладки, какое-то имя и с ними оперирует CUDA run-time. Символы в своем имени и мета-информации содержат данные о том, под какую CC собрано ядро. CMake, начиная с 3.8, умеет сам оперировать с CUDA. Никакой дополнительный файл не нужен. Он делает все внутри самостоятельно. В зависимости от версии, увеличивается количество фич.
OpenCL
Аналогичная структура и у OpenCL, но с некоторыми деталями. Аналогичная диаграмма с точностью до библиотеки libOpenCL.so.X. Она и API, которые входят в стандарт – это в некотором смысле фасад. В Ubuntu и других дистрибутивах в качестве пакета поставляется libOpenCL.so и набор соответствующих файлов. Даже не имея реализацию под конкретную платформу, вы их можете себе поставить и в свою программу скомпилировать. Этим и пользуется, в частности, OpenCV. libOpenCL.so умеет перечислять все доступные реализации от разных производителей и через API ими можно управлять. libOpenCL.so умеет загружать конкретную версию реализации и форвардить запрос к нему. У каждой вендорской реализации может быть свой драйвер ядра и другая специфика и на это уже каждый вендор ставят отдельно. Для NVidia, у NVidia есть пакет nvidia-libopencl1, nvidia-opencl-dev, который как раз и реализует нижний слой, вендорскую часть. Драйвер используется тот же самый, как и для CUDA. Для того, чтобы диагностировать текущий OpenCL, можно использовать утилиту clinfo, она становится сама из репозитория Ubuntu, она много где есть в стандартной поставке. clinfo запрашивает все доступные реализации OpenCL и печатает информацию в консоль.
Важный момент – в большинстве случаев вендорские реализации, кусочки, они умеют писать информацию про себя, не догружая драйвер и остальную часть. С этой утилитой надо быть аккуратнее. Его дефолтное поведение – хранить ядра в виде исходного кода. Они лежат в виде строк в программе скомпилированной. На ходу JIT компилятор компилирует под вашу платформу и ядра запускаются. Все расширения – это динамические библиотеки, которые подгружаются на лету. Проблема совместимости возникнет не в compile time, а в run-time, что-то не загрузится. OpenCL контекст, как и что касается OpenGL и прочих стандартов, он с thread-local. Вы не можете использовать OpenCL из нескольких потоков без специальных вызовов действий. Там, где вы создали контекст, в том потоке вы и должны его использовать. В OpenCV ядра тоже включены в виде набора строк внутрь исполняемого файла. Они компилируются на ходу JIT компилятором. OpenCV пишет свой кэш на диск и ядра повторно не перекомпилируются.
Конфликт run-time
Несколько слов о том, что делать с run-time и как быть, если вам надо построить сложную систему, где у вас несколько вещей используются сразу. Общее правило – если у вас несколько динамических библиотек, то все run-time должны быть между собой изолированы. Это касается C++ run-time, это касается CUDA, OpenCL, чего бы ни было. В частности, для CUDA – нельзя использовать несколько версий CUDA, несколько run-time одновременно и передавать между ними указатели или объекты. То же самое касается и памяти. До недавнего времени на Windows это было большой проблемой, если у вас сборка идет с Visual Studio, то используйте динамический run-time, одинаковый везде, либо лучше делайте аллокацию в пределах той же библиотеки, иначе будут коллизии в виде того, что alloc зовется из одного run-time, freeze зовется из другого и в этот момент все сломается. То же самое касается во многом OpenCL, CUDA и прочих других библиотек. Передавать объекты, линковать разные версии опасно. CUDA run-time, CUDA функции, они хоть и не имеют контекста в качестве параметра, он хранится неявно внутри библиотеки мета-информации. Он утилизируется при первом вызове функции CUDA. Один из хаков, которые рекомендуют NVidia – если вы хотите вызывать инициализацию run-time в какое-то предсказуемое время, вы можете сделать это с помощью cudaFree(null). Используйте cudaSetDevice, который управляет GPU, если у вас их несколько.
Практика
Теперь я продемонстрирую на практике часть из того, о чем я писал. Часть первая – htop. Как вы видите, почти из восьми гигабайт оперативной памяти больше половины покрашена оранжевым, это все у меня дисковый кэш, что с точки зрения ввода-вывода очень полезно. Я могу нажать L и тут видно все объекты ядра, практически все дескрипторы, в том числе пайпы, открытые файлы, много других полезных вещей. Если ваш процесс питонный, то тут вы видите все интерпретаторы, все загруженные в этот момент исполняемые файлы в том числе и питонные скрипты. Тут будет и OpenCV. Если вы не знаете, какой OpenCV у вас загрузился, вы здесь это увидите. Аналогично я нажимаю E и вижу весь environment, все переменные окружения.
Про исполняемые файлы. file – это полезная штука, которая показывает практически всю информацию. Вот у меня есть собранный DeviceQuery, который я вынул из сэмплов когда-то. Он про него мне явно скажет, что это исполняемый файл ELF, что динамический и not stripped, то есть там есть отладочные символы. Если символов нет – он напишет stripped. Тут же он написал архитектуру. file знает про все популярные архитектуры, на которых Linux бежит и про все даже неродные файлы вам скажет, какой он архитектуры.
Дальше давайте поговорим про readelf. readelf -d показывает динамическую часть от shared object. Вот на deviceQuery и поэкспериментируем. Мы видим здесь список использованных библиотек. Дальше идет список секций, адресов и так далее, размеров. Что нам здесь полезно – это зависимость, самая начальная часть. Дальше здесь будет SONAME, здесь же может быть RPATH, в нашем случае он вообще не прописан, не указан. А так здесь отдельная строчка RPATH и указан список путей, где ищутся зависимости.
Поговорим немножко про valgrind и про память. У меня есть пример, питонный тест, который что-то прогоняет с OpenCV, не суть – просто приложение на питоне. Мы можем взять valgrind, указать --tool=massif и написать, что это python3. Сейчас массив поработает и покажет картинку. Можно воспользоваться ms_print. Указать ему файл. Таблица получается, в моменте времени сколько памяти использовалось и так далее. Тут стеки вызовов, кто когда лоцировал память. А еще, если запустить massif-visualiser и указать ему этот файл, он рисует красивую диаграмму. Мы видим вот такую картинку, в которой подписаны все функции, которые смог забрать tool. У меня самосборный OpenCV без отладочных символов, поэтому про OpenCV здесь мало что видно. Если собрать его с символами, то все вызовы внутри OpenCV будут здесь подписаны, кто когда и сколько памяти выделил. Соответственно, пиковое потребление памяти становится очевидным.
Вернемся к консоли и посмотрим в деле GStreamer. Он позволяет строить пайплайны из тех компонентов, которые в GStreamer доступны, прямо из командной строки. Есть утилита gst-launch, которая их запускает. Вот пример простейшего пайплайна, gst-launch и два компонента – videotestsrc, который генерирует тестовую картинку, и ximagesink, который просто показывает окошко на экран с тем, что получилось. Если запустить в таком виде программу, то на экране мы увидим набор цветных полосок и какой-то белый шум, который генерирует утилита. То же самое можно использовать на питоне. В VideoCapture в параметре указывается строчка с пайплайном, OpenCV приклеивается к компоненту appsink. И явно указал, что мы хотим использовать GStreamer в качестве бекэнда. В результате запуска мы получим вот такую картинку. У меня получилось окно, которое показывает цветные полоски и кусочек белого шума. Это то, что сгенерировалось. Технически можно построить пайплайн с хардварным кодеком, с кастомной камерой и чем угодно, засунуть в приложение и потестировать в таком режиме. На практике это бывает очень полезно для автоматического тестирования.