О семантике C++
В продолжение кулуарного разговора с Highload++, навеянного 26-м слайдом презентации Андрея Аксенова.
Вот такой вот код:
char *m_sBuffer;
size_t m_iLimit, m_iCounter;
};
void someShit::try1()
{
m_iCounter = 0;
while (m_iCounter< m_iLimit && m_sBuffer[m_iCounter])
m_iCounter++;
}
void someShit::try2()
{
size_t l_iCounter = 0;
while (l_iCounter< m_iLimit && m_sBuffer[l_iCounter])
l_iCounter++;
m_iCounter = l_iCounter;
}
- gcc и Visual Studio генерируют код с той семантикой, какой написано;
- Intel C++ делает так же, если компилирует "библиотеку", но оптимизирует первый случай до второго, если все потроха (методы класса и вызывающий их main()) лежат в одном файле.
Ассемблерный код получается, по смыслу, такой вот (это MSVC-шный, как самый читаемый):
-
В первом случае инкрементруем счетчик прямо в памяти:
$LL2@try1:
mov ecx, DWORD PTR [eax+8]
cmp BYTE PTR [edx+ecx], 0
je SHORT $LN1@try1
inc ecx
mov DWORD PTR [eax+8], ecx
cmp ecx, DWORD PTR [eax+4]
jb SHORT $LL2@try1
$LN1@try1: -
Второй вариант инкрементирует регистр, а в память пишет на выходе:
$LL2@try2:
cmp BYTE PTR [edx+eax], 0
je SHORT $LN8@try2
inc eax
cmp eax, DWORD PTR [ecx+4]
jb SHORT $LL2@try2
$LN8@try2:
mov DWORD PTR [ecx+8], eax
Понятно что второй способ заметно эффективнее. Я не мерял, а Андрей намерял 1.75 раза разницы.
Вопросов никаких почти нет, кроме вот какого:
"Высокоуровневый ассемблер" делает "что сказал программист". Т.е. неявно предполагается, что какой-то другой thread (скажем, на другом CPU) может ВНЕЗАПНО, прямо вот в процессе выполнения цикла, поинтересоваться значением поля m_iCounter. Ну, типа, да, многопоточность, многопроцессорность, 21-й век. Интеловский компилятор, впрочем, умееет неявно догадаться, что ничего такого не будет, видя код main().
А вопрос такой: как бы сообщить компилятору явно, что никакого произвольного доступа не планируется (или, если и будет, результат его неважен) и что можно полноценно оптимизировать в регистрах, весь объект полностью в распоряжении. Хорошо бы еще сказать это переносимым способом (gcc/llvm/msvc/intel c++).
Comments
Вообще это какой-то жесткач. В общем случае другой тред може
Вообще это какой-то жесткач. В общем случае другой тред может не увидеть значение m_icounter даже если и через память всё работает (кэши, упорядоченность записи в пямять и прочие барьеры).
Компилироваться как в первом случае должно только если volatile явно указано.
Что же это получается, регистры вообще нельзя использовать для глобальных, статических и просто членов классов? Что-то тут не так.
Во-во. Ятакдумаю, что афтары MSVC et. al. просто еще не очен
Во-во. Ятакдумаю, что афтары MSVC et. al. просто еще не очень научились писать достаточно оптимизирующие компиляторы :-)
А кто научился? Я попробовал три компилятора, которые были
А кто научился?
Я попробовал три компилятора, которые были на винде - все три ведут себя одинаково (с оговоркой про Intel, который во многом подобном был замечен и ранее, например он некоторые мои бенчмарки оптимизировал до полного исчезновения т.к. видел что результат не используется).
Может не научились, а наоборот? Ну типа кэши сейчас быстрые
Может не научились, а наоборот? Ну типа кэши сейчас быстрые и т.д. если переменных не много, можно не париться, процессор сам внутри сделает?)
А чего он такого сделает? Аксенов же померял, разница в 1.75
А чего он такого сделает?
Аксенов же померял, разница в 1.75 раза, дохрена.
я к тому, что нет ответа на ваш вопрос просто плохой компи
я к тому, что нет ответа на ваш вопрос просто плохой компилятор. Не думаю, что есть предположения про второй тред. Стандарт явно говорит про volаtile. Вряд ли в GCC решили сознательно автоматом проставить volatile (а он на самом деле не про треды:) на все переменные члены класса в угоду плохим программистам.
Мне вот кажется на всяких классических RISC-ах первый вариант был бы ещё хуже чем на современных интелах. И вероятно GCC раньше так не делал.
Я бы согласился на "просто плохой компилятор", если бы не ув
Я бы согласился на "просто плохой компилятор", если бы не увидел у Интела в этом месте разумного поведения, зависящего от контекста.
Тогда для чистоты эксперимента нужно и с локальными переменн
Тогда для чистоты эксперимента нужно и с локальными переменными тест провести. Вставить после:
size_t l_iCounter = 0;
что-то типа
external_call(&l_iCounter);
Изменится поведение у интела?
если интел будет продолжать "видеть всю программу", то скоре
если интел будет продолжать "видеть всю программу", то скорее всего все останется так же (с жизнью с регистром), но я попробую вечерком.
Ну вот интел научился. Хотя я кажись в своих экспериментах н
Ну вот интел научился. Хотя я кажись в своих экспериментах несколько раз получил совершенно обратный результат - интел (девятый?) позорно облажался, а мелкософт выступил очень хорошо. Я тогда собственно бросил упражняться с компиляторами, решил что мелкософт - good enough for everybody, а что не гуд - руками перепишем.
Интел научился только если он видит весь контекст исполнения
Интел научился только если он видит весь контекст исполнения. Он много что в таком случае умеет, например узнать что данные получены через _mm_malloc и можно их грузить movaps, а не movups, много такого хорошего он умеет.
Но в реальной жизни, когда объектник компилируются, чтобы лечь в .DLL, которую будет неизвестно кто и неизвестно какими данными, в данном примере получается одинаковое говно.
И мой вопрос именно про то, как его избежать, сказав компилятору петушиное слово.
Да, по идее первый код - это именно volatile т.к. оно еще и
Да, по идее первый код - это именно volatile т.к. оно еще и перечитывает на каждой итерации.
И все будет работать по меньшей мере частично т.к. процессоры/ядра обмениваются информацией о протухших адресах в кэше и re-read будет, как минимум, читать из памяти.
Когда кэш сбрасывается - я никогда ранее не задумывался, оказывается есть инструкции и для целого кэша и для отдельного адреса....
вот именно "частично". Я вот другого не понимаю если не по
вот именно "частично". Я вот другого не понимаю если не пользоваться примитивами синхронизации (включая atomic функции) ведь ничего работать сейчас не будет. А если пользоваться, то всё будет хорошо, главное чтобы компилятор сбрасывал в пямять данные перед вызовами и возвратами функций.
В смысле, тогда эти неявные volatile и не нужны совсем.
В смысле, тогда эти неявные volatile и не нужны совсем.
Именно! Включая здравый смысл
Именно!
Включая здравый смысл - я ожидаю точки сброса в память именно на границах функций, плюс еще атомиков (которые могут не функциями быть, а спецмакросами).
Если мне зачем-то нужен постоянный сброс из регистра память - ну алле, в стандарте для этого уже 100 лет, как придумано отличное слово volatile.
Но нет - искуственный интеллект зачем-то делает неявный volatile для члена класса.
volatile - это re-read перед
volatile - это re-read перед каждым обращением, про write не написано :)
Но полностью разделяю негодование!
write сам собой вытекает, без
write сам собой вытекает, без него мало смысла в read.
И вообще, оно чуть ли не для интеракции с портами было введено, емнип :)
ну да, с memory mapped io
ну да, с memory mapped io (вроде как на старых санах), оно конечно имеет смысл: записать по адресу 7, а потом 11 может иметь другой смысл, чем просто 11.
Синхронизации нужны если несколько потоков пишут в одну loca
Синхронизации нужны если несколько потоков пишут в одну location. Это без вопросов.
А если второй поток только читает - то он получит какое-то значение. Так как без синхронизации - неизвестно в какую итерацию цикла он попадет, то любое значение ему подойдет.
ну вообще-то читать тоже нужно с синхронизацией. всякие пайп
ну вообще-то читать тоже нужно с синхронизацией. всякие пайпы, очереди и т.д. это как раз про это. Если второму треду можно забить на всё, включая порядок и т.д. то и оптимизация с регистрами ничего не испортит. Но по хорошему, это конечно кривость.
Интерес чтения без синхронизации ограничен
Уже запись 64 бит (size_t в вышеуказанном примере) не атомарна на интеловской архитектуре. И эти наполовину перезаписанные 64 бита ловятся на практике.
Re: Интерес чтения без синхронизации ограничен
В данном примере size_t 32-бита (т.к. код 32-битный).
Но вообще чтение чего-то, что быстро-быстро меняется вот прямо под пальцами - не имеет смысла. Использовать это значение как-то внятно нельзя.
Может везде явно ему намекать чтобы он переменную в регистре
Может везде явно ему намекать чтобы он переменную в регистре держал. И будет как бы явный не volatile. А компилятор уже либо сможет регистр использовать, либо нет.
А как это сделать для переменной - члена класса?
А как это сделать для переменной - члена класса?
наверное никак. Даже если я вообще переменную объявляю и не
наверное никак. Даже если я вообще переменную объявляю и не используют, её вряд ли какой-то компилятор трогать станет или удалять (у разработчиков компиляторов, какая-то фобия на эту ему)..
В классе объявляете? Ну еще бы не фобия, она может быть объ
В классе объявляете?
Ну еще бы не фобия, она может быть объявлена, к примеру, для правильного выравнивания.
У интела более серъёзная математика в компиляторе, ему надо
У интела более серъёзная математика в компиляторе, ему надо всякие MMX вместо циклов ставить и т.д..
А MSVC все эти изыски поддерживать не обязан. И вообще там, помойму до сих пор можно встретить ситуации когда он сохраняет какую-то переменную в память и тут же следом её загружает. Что-нибудь не такое явное, но похожее по смыслу:
mov [ebp+4],eax
mov eax,[ebp+4]
наверняка он просто код нужный сгенерить не в состоянии и проще ему цикл руками написать как надо..
2008-я и 10-я студии - неплохие компиляторы. Да, формально и
2008-я и 10-я студии - неплохие компиляторы. Да, формально интел умеет больше (векторизация, параллелизация), а на практике - нет.
про "что сказал"
Явного модификатора volatile на этом члене класса простите, нету.
В стандарте ничего про гарантии доступности из соседних тредов, простите, нету.
Там вообще слова "тред", простите, нету.
Многопоточные ошибки такие фокусы, простите, не исправляют. А наоборот, заметают под ковер и усложняют поимку и починку.
Поэтому, считаю, осознанно реализовывать такое поведение, аргументируя его заботой о тредах, это где-то между глупостью и вредительством.
Я ни разу не говорил компилятору, что переменная volatile, и я хочу новое значение в памяти немедленно после каждой операции. Компилятор делает НЕ то, что я ему сказал!
Тут же вот какая беда: есть
Тут же вот какая беда: есть оптимальное поведение (и в данном случае это было бы "закэшировать в регистре"), а есть "рассчитанное на плохую ситуацию".
Я готов принять, что по-умолчанию оно будет "рассчитано на дурака", если какое-то количество многопоточных ошибок (коих много в жизни) будет таким способом заметено под ковер, то и слава богу, они не проявятся в реальной жизни, всем будет легче.
Но я хочу иметь возможность это отключить.
Схожая история уже обсуждалась тут: http://blog.lexa.ru/2010/12/27/o_stepennoi_funktsii_openmp_i_prochikh_gr...
В VS2010 в OpenMP добавили спинлок, для синхронизации после завершения всех тредов. Отчего стало больно в тех случаях, когда worker-ы мелкие, оверхед большой. Зато какое-то количество ошибок исчезло.
Интел его тоже добавил, но сделал регулируемым.
данный пример к многопоточности отношения не имеет, это прос
данный пример к многопоточности отношения не имеет, это просто кривость или баг компилятора. компилятор по умолчанию генерирует код для однопоточного случая, применяет все известные оптимизации и не заботится об их корректности, это должен делать программист, если нужно.
попробуйте переписать в виде цикла for:
for(m_iCounter = 0; m_iCounter < m_iLimit && m_sBuffer[m_iCounter]; m_iCounter++) ;
Ну вот Intel C++ явно делает ЭТО как-то осмысленно.
Ну вот Intel C++ явно делает ЭТО как-то осмысленно.
Вообще это довольно древний момент, я не уверен что применим
Вообще это довольно древний момент, я не уверен что применимо в конкретном случае, но в целом - вот:
http://msdn.microsoft.com/en-us/library/aa984741(v=vs.71).aspx
Алиасинг - это совсем не то, это предположение, что два разн
Алиасинг - это совсем не то, это предположение, что два разных указателя могут, на самом деле, указывать в одну и ту же memory location и, соответственно, оптимизировать надо аккуратнее.
Но это может быть отрыжка от чего-то такого, да.
В смысле - два указателя в одном локальном куске кода.
В смысле - два указателя в одном локальном куске кода.
Ну я тоже написал, что про конкретный случай неясно. Хотя ме
Ну я тоже написал, что про конкретный случай неясно. Хотя механизм в принципе тот же - вычисление области видимости данных и хинты к нему, но увы.
Может, whole program optimization помогло бы, если вообще можно MSVC к примеру заставить это оптимизировать, что не факт.
Но непереносимо конечно ни разу и вообще немного не в ту степь.
Это pointer aliasing. Второй указатель - это this
Это pointer aliasing. Второй указатель - это this
А первый?
А первый?
m_sBuffer
m_sBuffer
В смысле, компилятор закладывается на то, что счетчик (котор
В смысле, компилятор закладывается на то, что счетчик (который и берется из памяти) может оказаться внутри буфера?
Интересная мысля, заменю указатель на массив...
Ага, оно. Если m_sBuffer определить как char[10k], то интел
Ага, оно.
Если m_sBuffer определить как char[10k], то интел умело начинает работать с регистром. Гы.
Сейчас пост про это напишу....
> Т.е. неявно предполагается, что какой-то другой thread (ск
> Т.е. неявно предполагается, что какой-то другой thread (скажем, на другом
> CPU) может ВНЕЗАПНО, прямо вот в процессе выполнения цикла,
> поинтересоваться значением поля m_iCounter.
Дык, а нахрена придумали слово volatile ?
> А вопрос такой: как бы сообщить компилятору явно, что никакого произвольного доступа не планируется
Дык, именно это и должно подразумеваться по умолчанию, а для всех остальных случаев есть volatile .
Кстати, для arm'ов RVCT вполне оптимизирует такую фигню и локализует переменуную в регистре с отложенным сбросом окончательного значения в память. А гэцеце - фиг.
Повторю аргумент, который уже тут всплывал по треду. Интел
Повторю аргумент, который уже тут всплывал по треду.
Интел умеет это оптимизировать. Но делает это только в том случае, когда видит "все приложение" (т.е. в один исходник с try1/try2 поместить еще и main(), который их вызывает).
А если main() вынести в отдельный файл - то и у интела остается работа через память.
Т.е. дело не в умении компилятора, а в каких-то assumptions, про которые я и пытаюсь спросить :)
> Т.е. дело не в умении компилятора, а в каких-то assumption
> Т.е. дело не в умении компилятора, а в каких-то assumptions, про которые я и пытаюсь спросить :)
Так в том то и дело, что какие-то это странные предположения.
У RVCT вроде нет таких предположений и если не написано volatile, то будет пытаться держать в регистрах. Я видел когда, когда несколько полей структуры были полностью загружены в регистры в начале функции в регистры, а потом сброшены те что поменялись.
Я, как положено приверженцу conspiracy theory, подразумеваю
Я, как положено приверженцу conspiracy theory, подразумеваю тут заговор.
Осталось выяснить, в чем же он заключается.
Т.е. компилятор могет, но не делает. И мне кажется что не по дурости не делает, а с каким-то умыслом.