Комментарии 17
Ой, ну если бы так всё было просто... Язык Си - стековый. Если вы не работаете через указатели, то структура сначала бросится в стек одного потока, потом заблокируется до возврата из функции обработки...
Так что пример негодный. Тормозить существенно из-за процессора в данном случае код не будет.
То, что вы описываете — это поведение обычного синхронного вызова функции.
Но в многопоточности на Си это так не работает:
1. В примере структура counters объявлена глобально.
2. Даже если создавать потоки, вы физически не можете передать туда структуру по значению.
Так что в реальном многопоточном коде потоки работают параллельно с общим куском памяти, никаких "блокировок до возврата" там нет.
Я имел ввиду, что пример не очень жизненный у вас. Всё таки кэш затем придуман, чтобы ускорять в первую очередь чтение (в том числе повторное чтение) одним потоком.
Но то, что проблема с пониманием работы аппаратуры есть, я не отрицаю.
Бенч на 500000000 показал такое мне до этого.
False Sharing: 8.062 секунд
выравнивание: 0.977 секунд
А зачем вообще складывать счетчики в общий массив? Объяви локальную переменную в стеке, и проблема даже не возникнет. Стек у каждого потока свой.
Суть примера в том, что нам нужно собирать общую статистику пакетов. Если каждый поток будет считать в свою локальную переменную:
1. Поток метрик не сможет их прочитать (стек приватен).
2. При завершении/пересоздании потока данные исчезнут вместе со стеком.
3. Чтобы как-то собрать итог, потокам все равно придется сбрасывать свои локальные суммы в глобальную переменную через Mutex или Atomic, создавая узкое горлышко.
Ну и вопрос обратный вам — зачем-то же придумали Sharded Counters, что по сути я здесь и рассказал? Есть ли у вас более жизненный пример для применения такого паттерна? ;)
Еще одной ценной добавкой к статье была бы демонстрация способов диагностики проблемы, даже на этом простом примере, чтобы у новичков было понимание, на какие метрики обратить внимание:)
Интересная статья, мне лично, очень в тему. Я как раз работаю над многопоточным проектом и наступил на озвученные в статье грабли. В связи с этим вопрос. В приложении несколько потоков работают с неким глобальным объектом, доступным по чтению. Помимо него, каждый поток имеет свои куски данных в общем массиве записей. Эти куски достаточно большие по размеру (десятки и сотни килобайт) и не пересекаются. Работа потоков заключается в сравнении некоторых значений из глобального объекта со своими данными. В однопоточном режиме скорость поиска порядка 0.5 секунды, а в многопоточном увеличиваеся до 2 секунд. При этом сам процессор имеет 14 ядер, 28 потоков. Правильно ли я понял, что чтение тоже создаёт блокировку работы с памятью или то что описано выше относится только к записи?
Нет, чистое чтение не создает блокировок и не вызывает False Sharing.
Протокол MESI (протокол когерентности кэшей) умный. Если ваши 14 ядер только читают один и тот же глобальный объект, эта 64-байтная кэш-линия получает статус Shared. Она мирно копируется в L1-кэши всех 14 ядер, и они работают с ней на максимальной скорости, не мешая друг другу. Никакого пинг-понга не происходит.
Какие могут быть у вас причины:
1. Скрытая запись - достаточно одной скрытой записи и все озвученое убивается.
2. Упор в шину памяти. 14 ядер / 28 потоков, одновременно прокачивающие через себя огромные массивы данных из RAM - плашки памяти физически (!) не успевают отдавать память и ядра простаивают.
3. Если ваш массив выделен одним потоком (и физически лежит в банке памяти процессора А), а обрабатывается потоками на процессоре Б, чтение происходит через медленную межпроцессорную шину.
Вам надо проверить на запись прежде всего, а также запустить профилировщик - если увидите огромный процент Memory Bound или L3 Cache Miss, значит уперлись в пропускную способность оперативки.
Большое спасибо за развёрнутый ответ!
Если это возможно, поделитесь, пожалуйста, результатами анализа ;) У вас интересный случай.
Проанализировал свой код, вооружившись новыми знаниями, почёрпнутыми в этой статье. Оказалось, что я, в целях отладки, использовал для сбора статистики ещё один глобальный массив из 4 целых переменных.Под многопоточной нагрузкой они блокировали доступ к этим счётчикам, поэтому было падение скорости работы почти в четыре раза по сравнению с однопоточным. Когда я переписал код (выделил каждому потоку дополнительно свой набор счётчиков и добавил между ними пустоты, чтобы куски по 64 байта не пересекались), то всё встало на свои места. Скорость поиска увеличилась в 8-9 раз (не в 28 к сожалению) по сравнению с однопоточным поиском, но для меня это уже терпимо.
Ещё раз спасибо за статью, пишите больше на эту тему!
Александр, это крутой результат на самом деле!
1. У вас 14 ядер, остальные 14 логических потоков делят кэши с физическими.
2. Частоты - если одно ядро и будет работать на Турбо, то когда загружены все ядра - Турбо отключается из-за контроля температуры.
3. Ну и шина памяти - скорее всего уперлись в нее тоже, так как объемы данных приличные.
Возможно, если хотите дальше дожимать оптимизацию, стоит посмотреть в векторизацию (SIMD-инструкции).

Невидимый враг многопоточности: False Sharing и кэш-линии процессора