Узкое место в производительности _L_unlock_16

Я пытаюсь использовать профилировщик процессора google perf tools для устранения проблем производительности многопоточной программы. В однопоточном режиме это занимает 250 мс, а в 4 нитях — около 900 мс.

В моей программе есть файл mmap, общий для всех потоков, и все операции доступны только для чтения. Также моя программа создает большое количество объектов, которые не разделяются между потоками. (В частности, моя программа использует библиотеку CRF ++ для выполнения запросов). Я пытаюсь выяснить, как заставить мою программу работать лучше с многопоточностью. График вызовов, созданный профилировщиком ЦП инструментов gperf, показывает, что моя программа проводит много времени (около 50%) в _L_unlock_16.

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

Краткое описание того, что делает моя программа. У меня есть несколько слов в файле (4). В моей программе у меня есть processWord (), который обрабатывает одно слово с использованием CRF ++. Этот processWord () — это то, что выполняет каждый поток. Мой main () читает слова из файла, и каждый поток запускает processWord () параллельно. Если я обрабатываю одно слово (следовательно, только 1 поток), это занимает 250 мс, и поэтому, если я обрабатываю все 4 слова (и, следовательно, 4 потока), я ожидаю, что оно завершится к тому же времени 250 мс, однако, как я упоминал выше, это займет около 900 мс.
Это каллиграф исполнения — https://www.dropbox.com/s/o1mkh477i7e9s4m/cgout_n2.png

Я хочу понять, почему моя программа тратит много времени на _L_unlock_16 и что я могу сделать, чтобы смягчить ее.

0

Решение

Еще раз _L_unlock_16 не является функцией вашего кода. Вы смотрели на следы выше эта функция? Каковы те, кто его вызывает, когда программа ждет? Вы сказали, что программа тратит впустую 50% ожидания внутри. Но какая часть программы заказала эту операцию? Это опять из памяти alloc / dealloc ops?

Кажется, эта функция взята из libpthread. CRF + обрабатывает потоки / libpthread каким-либо образом? Если да, то, возможно, библиотека плохо настроена? Или, может быть, он реализует некоторую «базовую безопасность потоков», добавляя блокировки повсюду и просто не подходит для многопоточности? Что об этом говорят доктора?

Лично я бы предположил, что он игнорирует потоки и что вы добавили все потоки. Я могу ошибаться, но если это правда, то CRF ++, вероятно, вообще не будет вызывать эту функцию «разблокировки», а «разблокировка» вызывается из вашего кода, который управляет потоками / замками / очередями / сообщениями и т. Д.? Остановите программу несколько раз и посмотрите, кто вызвал разблокировку. Если он действительно тратит 50% сидя в разблокировке, вы очень быстро узнаете, кто вызывает блокировку, и вы сможете либо устранить ее, либо, по крайней мере, провести более изысканное исследование.

РЕДАКТИРОВАНИЕ № 1:

Эх .. когда я сказал «stacktrace», я имел в виду stacktrace, а не callgraph. Callgraph может выглядеть красиво в тривиальных случаях, но в более сложных случаях он будет искаженным и нечитаемым и скроет ценные детали в «уплотненную» форму. Но, к счастью, здесь случай выглядит достаточно простым.

Пожалуйста, обратите внимание на начало: «Слово процесса, 99x». Я предполагаю, что «99x» является счетчиком вызовов. Затем посмотрите на «tagger-parse»: 97x. От этого:

  • 61x в rebuildFeatures, из которых 41х идет прямо в разблокировку и 20 (13) косвенно в него
  • 23x идет на сборку решетки для которой 21x идет на разблокировку

Я бы Угадай что это был CRF ++, довольно сильно использует блокировку. Мне кажется, что вы просто наблюдаете эффекты внутренней блокировки CRF. Это, конечно, не без блокировки внутри.

Кажется, он блокируется хотя бы один раз для «processWord». Трудно сказать, не глядя на код (это с открытым исходным кодом? Я не проверял ..), из трассировки стека это было бы более очевидно, но ЕСЛИ он действительно блокирует один раз для «processWord», что это может быть даже » глобальная блокировка «, которая защищает» все «от» всех потоков «и вызывает сериализацию всех заданий. Без разницы. В любом случае, очевидно, что внутренности CRF ++ блокируются и ждут.

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

Если объекты CRF являются общими, отмените их и см. Выше.

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

Последний совет может показаться странным (он работает медленно, верно? Так зачем его игнорировать?), Но на самом деле это самый важный совет, и вы должны сначала его попробовать. Если параллельные задачи имеют похожий «профиль данных», то весьма вероятно, что они попытаются установить одинаковые блокировки в один и тот же приблизительный момент времени. Представьте себе кэш среднего размера, в котором хранятся слова, отсортированные по первой букве. На верхнем уровне находится массив, скажем, 26 записей. Каждая запись имеет блокировку и список слов внутри. Если вы запустите 100 потоков, каждый из которых сначала проверит «мама», затем «папа», а затем «сын» — тогда все эти 100 потоков будут сначала попадать и ждать друг друга в точке «М», затем в точке «D», а затем в точке «S». ». Ну, примерно / вероятно, конечно. Но ты получил идею. Если бы профиль данных был более случайным, то они бы гораздо меньше блокировали друг друга. Имейте в виду, что обработка ОДНОГО слова — небольшая задача, и вы пытаетесь обработать одно и то же слово. Даже если внутренняя блокировка CRF разумна, она просто обязана попасть в те же области. Попробуйте еще раз с более рассредоточенными данными.

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

Подготовьте набор из 100 или более слов. Запустите и измерьте его на одном потоке. Затем разделите слова в случайном порядке и запустите их на 2 и 4 темы. И измерить. Если не лучше, попробуйте на 1000 и 10000 слов. Чем больше, тем лучше, конечно, имея в виду, что тест не должен длиться до вашего следующего дня рождения;)

Если вы заметили, что 10 000 слов, разделенных на 4 потока (2500 Вт на тысячу), работают примерно на 40% -30% — или даже на 25% быстрее, чем в одном потоке — вот и все! Вы просто дали ему слишком маленькую работу. Он был адаптирован и оптимизирован для больших!

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

2

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]