Может ли компилятор C / C ++ легально кэшировать переменную в регистре через вызов библиотеки pthread? - PullRequest
9 голосов
/ 18 декабря 2010

Предположим, что у нас есть следующий бит кода:

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

void guarantee(bool cond, const char *msg) {
    if (!cond) {
        fprintf(stderr, "%s", msg);
        exit(1);
    }
}

bool do_shutdown = false;   // Not volatile!
pthread_cond_t shutdown_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t shutdown_cond_mutex = PTHREAD_MUTEX_INITIALIZER;

/* Called in Thread 1. Intended behavior is to block until
trigger_shutdown() is called. */
void wait_for_shutdown_signal() {

    int res;

    res = pthread_mutex_lock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not lock shutdown cond mutex");

    while (!do_shutdown) {   // while loop guards against spurious wakeups
        res = pthread_cond_wait(&shutdown_cond, &shutdown_cond_mutex);
        guarantee(res == 0, "Could not wait for shutdown cond");
    }

    res = pthread_mutex_unlock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not unlock shutdown cond mutex");
}

/* Called in Thread 2. */
void trigger_shutdown() {

    int res;

    res = pthread_mutex_lock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not lock shutdown cond mutex");

    do_shutdown = true;

    res = pthread_cond_signal(&shutdown_cond);
    guarantee(res == 0, "Could not signal shutdown cond");

    res = pthread_mutex_unlock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not unlock shutdown cond mutex");
}

Может ли совместимый со стандартами компилятор C / C ++ когда-либо кэшировать значение do_shutdown в регистре при вызове pthread_cond_wait()? Если нет, то какие стандарты / пункты гарантируют это?

Компилятор мог гипотетически знать, что pthread_cond_wait() не изменяет do_shutdown. Это кажется невероятным, но я не знаю ни одного стандарта, который бы мешал этому.

На практике, любые компиляторы C / C ++ кэшируют значение do_shutdown в регистре при вызове pthread_cond_wait()?

Какие вызовы функций гарантированно не компилируются компилятором для значения do_shutdown? Понятно, что если функция объявлена ​​извне и компилятор не может получить доступ к ее определению, он не должен делать никаких предположений относительно ее поведения, поэтому он не может доказать, что он не имеет доступа к do_shutdown. Если компилятор может встроить функцию и доказать, что он не имеет доступа к do_shutdown, то может ли он кэшировать do_shutdown даже в многопоточной настройке? Как насчет не встроенной функции в том же модуле компиляции?

Ответы [ 4 ]

6 голосов
/ 18 декабря 2010

Конечно, текущие стандарты C и C ++ ничего не говорят по этому вопросу.

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

Когда стандарт говорит, что мьютексы «синхронизируют доступ к памяти», реализации должны предполагать, что это означает, что изменения, сделанные под блокировкой в ​​одном потоке, будут видны под блокировкой в ​​других потоках.Другими словами, необходимо (хотя и недостаточно), чтобы операции синхронизации включали в себя барьеры памяти того или иного типа, а необходимое поведение барьера памяти заключается в том, что он должен предполагать, что глобальные переменные могут изменяться.

Потоки не могутБыть реализованным в виде библиотеки охватывает некоторые конкретные вопросы, которые требуются для фактического использования pthreads, но не указаны явно в стандарте Posix на момент написания (2004).Становится весьма важным, согласен ли ваш компилятор или тот, кто определил модель памяти для вашей реализации, с Бемом, что означает «пригодный для использования», с точки зрения того, чтобы позволить программисту «убедительно рассуждать о правильности программы».

Обратите внимание, что Posix не гарантирует целостного кэша памяти, поэтому, если ваша реализация извращенно хочет кешировать do_something в регистр в вашем коде, тогда , даже если вы пометили его как энергозависимый , возможно, она извращенно выберет незагрязнил локальный кеш вашего процессора между операцией синхронизации и чтением do_something.Так что, если поток записи работает на другом ЦП со своим собственным кешем, вы можете даже не увидеть изменения.

Это (одна из причин), почему потоки не могут быть реализованы просто как библиотека.Эта оптимизация извлечения изменчивой глобализации только из локального кэша ЦП будет допустимой в однопоточной реализации C [*], но нарушает многопоточный код.Следовательно, компилятору необходимо «знать» о потоках и о том, как они влияют на функции других языков (например, за пределами pthreads: в Windows, где кэш всегда согласован, Microsoft прописывает дополнительную семантику, которую она предоставляет volatile в несколькихрезьбовой код).По сути, вы должны предположить, что если ваша реализация столкнулась с проблемой обеспечения функций pthreads, то у нее возникнет проблема определения работоспособной модели памяти, в которой блокировки фактически синхронизируют доступ к памяти.

Если компилятор может встроить функцию и доказать, что он не имеет доступа к do_shutdown, то может ли он кэшировать do_shutdown даже в многопоточной настройке?Как насчет не встроенной функции в том же модуле компиляции?

Да для всего этого - если объект является энергонезависимым, и компилятор может доказать, что этот поток не изменяет его (либо через его имя, либо через указатель с псевдонимом), и если не возникает барьеров памяти, он может повторно использовать предыдущие значения.Конечно, могут быть и будут другие специфические для реализации условия, которые иногда останавливают его.

[*] при условии, что реализация знает, что глобальный объект не расположен по какому-то «специальному» аппаратному адресу, который требует, чтобы чтение всегда происходилочерез кэш в основную память, чтобы увидеть результаты любого аппаратного операции, влияющего на этот адрес.Но чтобы разместить глобальный объект в любом таком месте или сделать его специальным с помощью DMA или чего-либо еще, требуется волшебство, зависящее от реализации.В отсутствие какой-либо такой магии реализация в принципе может иногда знать это.

2 голосов
/ 02 октября 2011

Ответы от размахивания руками неправильные. Извините, что резкий.

Нет способа

Компилятор мог гипотетически узнать, что pthread_cond_wait () не изменяет do_shutdown.

Если вы считаете иначе, пожалуйста, покажите доказательство: полная программа C ++, такая, что компилятор, не предназначенный для MT, может сделать вывод, что pthread_cond_wait не изменяет do_shutdown.

ЭтоАбсурд, компилятор не может понять, что делают pthread_ функции, если он не обладает встроенным знанием потоков POSIX.

2 голосов
/ 18 декабря 2010

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

Насколько я знаю, об этом ничего прямо не сказано в стандарте, за исключением того, что (одиночныймногопоточная абстрактная машина, используемая стандартом для определения поведения выражений, указывает на то, что переменная должна быть прочитана при обращении к ней в выражении.Стандарт разрешает оптимизировать чтение переменной только в том случае, если можно доказать, что поведение «как если бы» было перезагружено.И это может произойти, только если компилятор может знать, что значение не было изменено вызовом функции.

Кроме того, библиотека pthread не дает определенных гарантий относительно барьеров памяти для различных функций, включая pthread_cond_wait(): Гарантирует ли защита переменной мьютексом pthread, что она также не кэшируется?

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

Так что в этом случаеВы должны будете использовать механизмы, чтобы гарантировать, что значение было перезагружено через вызов.Обратите внимание, что из-за аппаратных сложностей ключевое слово volatile может оказаться недостаточным для обеспечения правильного упорядочения доступа к памяти - для этого вам следует полагаться на API, предоставляемые pthreads или ОС.(Как примечание, последние версии компиляторов Microsoft действительно документируют, что volatile применяет полный барьер памяти, но я читал мнения, которые указывают, что это не требуется стандартом).

0 голосов
/ 11 января 2011

По моей собственной работе я могу сказать, что да, компилятор может кэшировать значения через pthread_mutex_lock / pthread_mutex_unlock. Я провел большую часть выходных, отыскивая ошибку в небольшом количестве кода, которая была вызвана кэшированием набора указателей и недоступностью для потоков, которые в них нуждались. В качестве быстрого теста я обернул назначения в блокировку / разблокировку мьютекса, и потоки все еще не имели доступа к правильным значениям указателя. Перемещение назначений указателей и связанной блокировки мьютекса в отдельную функцию решило проблему.

...