увеличить размеры структуры данных потока на смешной стороне? - PullRequest
3 голосов
/ 25 июля 2011

Компилятор: clang ++ x86-64 в Linux.

Прошло много времени с тех пор, как я написал какой-нибудь сложный системный код низкого уровня, и я обычно программирую против системных примитивов (windows и pthreads / posix). Итак, входы и выходы выскользнули из моей памяти. Я сейчас работаю с boost::asio и boost::thread.

Чтобы эмулировать синхронный RPC для исполнителя асинхронной функции (boost::io_service с несколькими потоками io::service::run, где запросы io_serviced::post '), я использую примитивы повышения синхронизации. Ради любопытства я решил sizeof примитивов. Это то, что я вижу.

struct notification_object
{
  bool ready;
  boost::mutex m;
  boost::condition_variable v;
};
...
std::cout << sizeof(bool) << std::endl;
std::cout << sizeof(boost::mutex) << std::endl;
std::cout << sizeof(boost::condition_variable) << std::endl;
std::cout << sizeof(notification_object) << std::endl;
...

Выход:

1
40
88
136

Сорок байтов для мьютекса ?? ?? ? WTF! 88 для условия Пожалуйста, имейте в виду, что я отталкиваюсь от этого раздутого размера, потому что я думаю о приложении, которое может создать сотни notification_object

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

Ответы [ 4 ]

23 голосов
/ 25 июля 2011

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

for (;;) {
    lock(lockA);
    unlock(lockA);
}

и

for (;;) {
    lock(lockB);
    unlock(lockB);
}

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

Следовательно, даже если фактический размер данных примитивного типа данных, лежащий в основе спин-блокировки или мьютекса, может быть только байтом или 32-битным словом, эффективный размер данных такого объекта часто больше.

Имейте это в виду, прежде чем утверждать "мои мьютексы слишком велики". На самом деле, в x86 / x64 40 байтов слишком малы , чтобы предотвратить ложное совместное использование, так как в настоящее время в кэше есть не менее 64 байтов.

Кроме того, если вы сильно обеспокоены использованием памяти, учтите, что объекты уведомлений не обязательно должны быть уникальными - переменные условия могут служить для запуска различных событий (через predicate, о котором знает boost::condition_variable). Поэтому было бы возможно использовать одну пару мьютекс / CV для всего конечного автомата вместо одной такой пары на состояние. То же самое касается, например, синхронизация пула потоков - иметь больше блокировок, чем потоков, не обязательно выгодно.

Редактировать: Еще несколько ссылок на «ложное совместное использование» (и отрицательное влияние на производительность, вызванное размещением нескольких атомарно-обновляемых переменных в одной и той же кэшированной строке), см. (Среди прочих) следующие публикации SO :

Как уже говорилось, при использовании нескольких «объектов синхронизации» (будь то атомно-обновляемые переменные, блокировки, семафоры ...) в многоядерной конфигурации кэша на ядро, разрешите каждому из них отдельная кешлайн пространства. Вы торгуете использованием памяти для масштабируемости здесь, но на самом деле, если вы попадаете в регион, где вашему программному обеспечению требуется несколько миллионов блокировок (то есть в ГБ памяти), у вас либо есть финансирование на несколько сотен ГБ памяти (и сотен ядер ЦП), или вы что-то не так делаете в дизайне своего программного обеспечения.

В большинстве случаев (блокировка / атом для конкретного экземпляра class / struct) вы получаете «заполнение» бесплатно, если экземпляр объекта, который содержит атомарную переменную, достаточно велик.

19 голосов
/ 25 июля 2011

На моем 64-битном Ubuntu box:

#include <pthread.h>
#include <stdio.h>

int main() {
  printf("sizeof(pthread_mutex_t)=%ld\n", sizeof(pthread_mutex_t));
  printf("sizeof(pthread_cond_t)=%ld\n", sizeof(pthread_cond_t));
  return 0;
}

печать

sizeof(pthread_mutex_t)=40
sizeof(pthread_cond_t)=48

Это означает, что вы утверждаете, что

Этот уровень накладных расходов на мобильность кажется смешным, может кто-то оправдать это для меня? насколько я помню эти примитивы должны быть 4 или 8 байтов в ширину в зависимости от модели памяти процессора.

совсем не соответствует действительности.

Если вам интересно, откуда берутся дополнительные 101 байт, взятые boost::condition_variable, класс Boost использует внутренний мьютекс.

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

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

6 голосов
/ 25 июля 2011

Глядя на реализацию:

class mutex : private noncopyable
{
public:
    friend class detail::thread::lock_ops<mutex>;

    typedef detail::thread::scoped_lock<mutex> scoped_lock;

    mutex();
    ~mutex();

private:
#if defined(BOOST_HAS_WINTHREADS)
    typedef void* cv_state;
#elif defined(BOOST_HAS_PTHREADS)
    struct cv_state
    {
        pthread_mutex_t* pmutex;
    };
#elif defined(BOOST_HAS_MPTASKS)
    struct cv_state
    {
    };
#endif
    void do_lock();
    void do_unlock();
    void do_lock(cv_state& state);
    void do_unlock(cv_state& state);

#if defined(BOOST_HAS_WINTHREADS)
    void* m_mutex;
#elif defined(BOOST_HAS_PTHREADS)
    pthread_mutex_t m_mutex;
#elif defined(BOOST_HAS_MPTASKS)
    threads::mac::detail::scoped_critical_region m_mutex;
    threads::mac::detail::scoped_critical_region m_mutex_mutex;
#endif
};

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

class mutex : private noncopyable {
private:
#if defined(BOOST_HAS_WINTHREADS)
    void* m_mutex;
#elif defined(BOOST_HAS_PTHREADS)
    pthread_mutex_t m_mutex;
#elif defined(BOOST_HAS_MPTASKS)
    threads::mac::detail::scoped_critical_region m_mutex;
    threads::mac::detail::scoped_critical_region m_mutex_mutex;
#endif
};

Так что кроме noncopyable я не вижу особых накладныхэтого не происходит с системным мьютексом.

3 голосов
/ 02 апреля 2014

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

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

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

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

...