13.7.10

Ворчания пост e02

Войны с компиляторами продолжаются! На сей раз под руку Маньяку попал ни кто иной, как великий и ужасный MSVC собственной персоной.

Начну я издалека. Представьте себе класс.. Да что там представлять? Вот он:
struct X
{
void SomeMethod() {}
X SomeOtherMethod() { return X(); }
};
То есть, класс с методом, возвращающим экземпляр этого класса. Компилируется? Компилируется. И работает.
А теперь давайте дружно представим, что код X x; x.SomeMethod(); генерит "C2039: SomeMethod is not a member of X". Не получается? Я вам помогу. :) Для этого код придётся "слегка" усложнить.
Нам понадобится: (1)шаблон класса хотя бы с двумя параметрами, (2)неполная специализация этого шаблона с (3)содержимым, похожим на содержимое X.
// 1
template<int A, int B>
struct X
{
};

// 2
template<int A>
struct X<A,0>
{
// 3
void SomeMethod() {}
X<0,0> SomeOtherMethod() { /*не суть*/ }
};
Пишем X<0,0> x; x.SomeMethod(); и напарываемся на то, за что и боролись:
error C2039: 'SomeMethod' : is not a member of 'X<A,B>'
with
[
A=0,
B=0
]
То есть, в переводе на человеческий, компилятор нам английским по белому туманно намекает: "X<0,0>::SomeMethod не является членом X<0,0>". Весело, да?

А теперь мы будем шаманить.
Определим SomeMethod в базовом шаблоне:
template<int A, int B>
struct X
{
void SomeMethod() { assert(!"X<A,B>::SomeMethod()"); }
};
/*
остальное - как раньше
*/
О, чудо! Никаких ошибок.. при компиляции. По наличию ассерта вы, вероятно, уже поняли, что он срабатывает. Другими словами: вызвается метод в базовом шаблоне, будто специализации и нет вовсе! Весело, да?

Ещё нет? Тогда продолжим наше шаманство!
Откатим предыдущее изменение и поменяем определение SomeOtherMethod в специализации на такое:
void SomeOtherMethod( X<0,0> x ) { (void)x; }
Функция теперь не возвращает X<0,0>, и, не смотря на использование этого типа в другом месте, весь код gracefully компилируется. Весело, да?

Уже пробивает на нездоровое гыгы? Всё равно шаманим дальше.
Вернёмся к изначальному виду функции SomeOtherMethod и отрежем ей тело:
X<0,0> SomeOtherMethod();
Вы, поди, уже догадались, но я всё же озвучу: Взлетело. Уже не так смешно, правда?)
Совсем грустно вам станет, когда я напишу
template<int A>
inline X<0,0> X<A,0>::SomeOtherMethod()
{
}
вне класса и скажу, что и так оно тоже взлетело.

Ну и дабы вам окончательно поплохело:
1. Изменив возвращаемое значение с X<0,0> на X<A,0> мы волшебным образом ликвидируем еггог.
2. Изменив возвращаемое значение с X<0,0> на X<A,0> & или X<A,0> * мы опять же избавляемся от ошибки.
3. Добавив в специализацию член X<0,0> m_x, получаем точно такую же ошибку вместо ожидаемого "C2460: X<0,0>::m_x uses X<0,0>, which is being defined".

Баг? Maybe, maybe.. Но как он выжил в течение всего срока существования MSVC 8, 9, 10 и наверняка более ранних? Я не знаю, что говорит по этому поводу стандарт, но сдаётся мне: майкрософт при делах. =\

13 комментариев:

  1. Знаешь что, Маньяк, ИМХО проблема здесь в циклической зависимости. Ибо X<0,0> попадает под специализацию X, и при попытке инстанцировать X<0,0> компилятор проваливается в бесконечный цикл. А как известно имеется некоторый предел вложенности после которого попытки прекращаются и метод остается неопределенным. Подтверждением может служить то, что написав

    X<0,1> SomeOthherMethod() {...}

    мы избавляемся от проблемы, ибо X<0,1> не попадает под специализацию X и благополучно инстанцируется.

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

    ОтветитьУдалить
  2. Такая версия посещала одной из первых, но пришлось её отбросить. :) И вот почему:

    X<A,0> SomeOtherMethod() {...}

    компилится, не смотря на то, что X<A,0> в данном случае это ни что иное, как X<0,0>, ибо создаём-то мы экземпляр X<0,0>.
    Да и циклические зависимости в шаблонах, как показывает моя практика, приводят к внутренней ошибке компилятора..) Не в тему будет сказано, но лимит на глубину шаблонной рекурсии у мсвц довольно забавный: 496. Да-да, именно столько, а не 4096. :D

    Ну и к тому же, неясной остаётся избирательность компилятора: возвращаемый тип таким делать нельзя, а аргумент почему-то можно) Да к тому же, ошибка исчезает, если убрать тело функции. оО

    Всё-таки интересно было бы посмотреть, что в стандарте про такие извращения написано.. Но уж больно много букв, и, к тому же, 03 в свободном доступе вроде нет, только 98. :(

    ОтветитьУдалить
  3. Ну X(А,0) - это синоним X в данном случае, и посему он не будет рекурсивно инстанцироваться, ибо инстанцируется он в данный момент, ИМХО.

    Что до избирательности, то думаю, что для аргумента нам нужен лишь размер объекта класса знать, а методы никак на размер объекта не влияют, посему рекурсивно инстанцировать все методы с их аргументами и возвращаемыми значениями смысла нет. Хотя вообще странно что инсанцируются неиспользуемые методы, по идее для них только синтаксическая корректность проверяться должна.

    Что до ошибок, то опять же по моему скромному мнению, компилятор пытаясь подобрать специализацию, но встретив в ней ошибку (бесконечная рекурсия), просто отвергает данную специализацию и предлагает в качестве единственного варианта исходный X, в котором, естественно, SomeMethod отсутствует.

    Т.е. по-моему это все таки циклическая зависимость, которая в принципе возникнуть не должна, ибо этот метод инстанцироваться в принципе не должен. А что до стандарта, то копаться мне в нем тоже лень было, но под GCC Comeau прекрасно компилируется.

    ОтветитьУдалить
  4. > Ну X(А,0) - это синоним X в данном случае
    Хм, может быть, может быть..

    > просто отвергает данную специализацию
    Даже не вякнув никаким ворнингом. =\ А ведь у M$ обычно на каждый чих - предупреждение. Нде.

    > под GCC Comeau прекрасно компилируется
    Хехе, ну так и знал, микромягкие накосячили. =)
    Надо будет мне ещё на gcc проверить.

    ОтветитьУдалить
  5. > А ведь у M$ обычно на каждый чих - предупреждение
    Ну не скажи... M$ намного более сдержан в комментариях, он и про порядок инициализации молчит и про отсутствие виртуального деструктора, и при присвоения в условиях... И вообще очень многое допускает, чего стандарт не допускает, да. Я раз додумался vallaray для не POD объектов использовать, так был очень удивлен когда под GCC мой код перестал выполнятся корректно. Под M$ и зависимые типы можно без typename использовать, и многие имена без явной квалификации. Так шо M$ очень даже удобно, но с переносимостью у него очень большие проблемы, хотя может это такая политика у них, что не удивительно.

    ОтветитьУдалить
  6. Хехе, ну, мне видимо не с чем сравнивать) Я кроме мсвц только с gcc слегка имел дело, да и то - в контексте чистого си.

    Интересно, а что именно другие компиляторы про порядок инициализации говорят?) Просто предупреждение при любом использовании extern объекта до входа в main? Oo
    Кстати присвоение в условиях он таки ловит, кроме случаев вроде if((x=y)==z), что в общем-то вполне православно. :)

    А их излюбленное "microsoft specific" - оно, да, с одной (тёмной >:)) стороны бывает очень удобно и хорошо. Например автоматическая установка мемори-барьеров вокруг операций над volatile и даже по-моему их автоблокировка в случае встроенных типов. Но портируемости мешает, печаль.. =\

    зы: Только я закончил основные работы над своим векторным классом с simd-апчхимизацией, как узнаю про valarray. :D:D Но всё равно его не брошу, потому что он хороший. Не зря же я "учил" компилятор раскатывать циклы. :) А в valarray, я смотрю, всё for-ами делается, а, как выяснилось, даже при заранее известном кол-ве итераций компилятор форы не раскатывает.
    Кстати, как раз искал, с чем бы производительность сравнить. >)

    ОтветитьУдалить
  7. >Просто предупреждение при любом использовании extern объекта до входа в main
    Нет, я имел в виду несоответствие порядка объявления и порядка инициализации членов в конструкторе.

    >Кстати присвоение в условиях он таки ловит
    Ну не знаю что он там у тебя ловит, но у меня на код вида

    int a = 0;
    if ( a = 2 ) {...}

    молчит как рыба об лед.

    >Только я закончил основные работы над своим векторным классом с simd-апчхимизацией
    Интересно посмотреть, я тоже этими делами интересуюсь

    >Не зря же я "учил" компилятор раскатывать циклы
    Учил? Разве он не умеет? Вот код (http://codepad.org/OtGyt93m), у меня на -О2 он прекрасно анроллит все, причем заметь во что превращается add_u. Этот ужас меня давно отучил вручную раскручивать циклы.

    А насичет valarray, так я тоже удивлен что он не анроллится. Но насколько я помню, Интеловский компилятор очень хорошо натаскан на такие вещи и прекрасно справляется с этим недоразумением.

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

    ОтветитьУдалить
  8. Гугл сцуко! Такую простыню текста похерил! Заново писать. -__-

    > несоответствие порядка объявления и порядка инициализации
    Ах это.. Да, зловредно. Я раньше не задумывался, в каком из порядков инициализация происходит, вот теперь буду точно знать)

    > молчит как рыба об лед
    /W4 :)

    > Интересно посмотреть
    Ы, можем в аське пересечься, скину тебе своего ужоснаха. :D

    > Этот ужас меня давно отучил вручную раскручивать циклы.
    Почему ужас? Благодаря отсутствию цикла, компилятор смог глубже заоптимайзить код: уменьшив кол-во итераций основного цикла (раз в 8 вроде), и соответственно увеличив объём работы выполняемый за одну итерацию. По-моему клёво. :)
    А если бы в цикле производилось несколько операций (скажем, сложение трёх массивов) то в цикле "по 32" у компилятора куда больше простора для перетасовки инструкций, чем в цикле "по четрые".

    > я тоже удивлен что он не анроллится
    Вот и у меня код не хотел анроллится ни в какую. =\ За юзеров icc я конечно рад, но хотелось бы, чтобы раскатывалось везде. :) В общем, чтобы контроль исходил то программиста, а не от левой ноги компилятора. :)

    > каковы побуждения преследовали
    Спортивный интерес, как обычно)) Хотелось пощупать SIMD. Так что - сначала сделать, а потом найти применение. :D И пока кроме партикловой системы применений-то не особо и видно. Попробую наверное её написать )
    А с одиночными векторами, конечно, выигрыш практически нулевой, но не думаю что проигрыш. К примеру любая бинарная арифметика тут - две загрузки в регистры, арифм. инструкция и выгрузка из регистра (для флоатов получается три movaps и, скажем, addps). А если считать по-компонентно, то выходит в разы толще)
    Но использовать одиночные вектора всё же не шибко удобно из-за необходимости 16-байтового алайна. Проигрыш в юзабельности.

    ОтветитьУдалить
  9. >Почему ужас? Благодаря отсутствию цикла, компилятор смог глубже заоптимайзить код
    Ну не скажи... По-моему та разность в оверхедах на итерацию с головой перекрывается возможностью промахов по кешам при распухшем коде. Ну и вообще это пустая болтовня без реальных замеров, могу по собственному опыту сказать, что иногда корень падения производительности может лежать очень глубоко, например в ассоциативности кеша, и чтение или запись по адресам, кратным некоторому значению окажется ужасом.

    ОтветитьУдалить
  10. > пустая болтовня без реальных замеров
    Сделал замеры :)
    add_u у меня побеждает с отрывом в несколько процентов) На всякий пожарный менял два основных цикла местами, результат слегка менялся, но add_u по-прежнему побеждал.

    Но вот попытки перехитрить оптимизатор обычно проваливаются, так что ручной анролл надо конечно применять очень осторожно..
    Попробовал я тут свой вариант:
    for( int i = 0; i < N/8; i++ )
    for_each<32>( res+i*32, op1+i*32, op2+i*32, binop<ADD,float> );
    Бегает чуть ли не шустрее add_u, но код идущий следом замедляется на треть. х_х По-видимому, кеш забивается кодом расплющенного цикла, хотя, казалось бы, не такой уж он и толстый. оО

    Кстати опробовал simd) Почти в 1.5 раза шустрее. Работает! =))

    ОтветитьУдалить
  11. > Сделал замеры :)
    Ну собсно, что и следовало доказать... Выигрыш та он выигрышем, но это абсолютно синтетический пример в котором к тому же узким местом является подсистема памяти, и любые незаметные телодвижения в оной области запросто меняют положение чаши весов и вообще наводят тотальный бардак (твой пример с ручным раскручиванием это доказывает). И была бы эта проблема реальной, то здесь в первую очередь не цикл раскручивать стоит, а увеличивать отношение арифметических операций к операциям чтения-записи.

    >Кстати опробовал simd)Почти в 1.5 раза шустрее
    У меня в 3 раза получилось шустрее. Но вот попытки дополнительно анроллить цикл( благо xmm регистры позволяют) ни к чему хорошему не привели, увы. Вот вам и пироги с котятами. Вот код http://codepad.org/ZEwnJrcz

    ОтветитьУдалить
  12. Кстати, если не боян, то мастрид конечно http://support.amd.com/us/Processor_TechDocs/25112.PDF

    http://www.intel.com/Assets/PDF/manual/248966.pdf

    И насичет ГЛЯ, то у меня все работает ))

    ОтветитьУдалить
  13. Ыть, давно меня здесь не было(

    > узким местом является подсистема памяти, и любые незаметные телодвижения в оной области запросто меняют положение чаши весов и вообще наводят тотальный бардак
    Ыгы, к сожалению.
    А ещё говорят "программирование - точная наука". :( Хрентам, шаманство чистой воды.

    > в первую очередь не цикл раскручивать стоит, а увеличивать отношение арифметических операций к операциям чтения-записи
    Ну, как я там выше говорил, первое способствует второму при нескольких операциях над одними данными (MISD). Если циклы не расплющатся, они будут служить барьерами при оптимизации, так что например v1 + v2 * v3 никогда не выродится в пачку madd-ов. Воотъ)

    > У меня в 3 раза получилось шустрее.
    Я тогда очень глупый ляп допустил у себя, позже исправил, и получилось примерно в 2. Хммм.

    А с префетчем мне ещё только предстоит познакомится поближе. :) Как раз собирался после возни с SIMD. Главное, что пока не могу понять, это возможно ли рассчитать его дальность? оО Тут, получается, надо знать, какое время займёт кещирование линейки и выполнение итерации алгоритма.. Иначе опять всё выливается в шаманство, которое на одной железяке будет давать десятикратный буст, а другую вообще замедлять. =\ Так что ссылки ты кинул очень вовремя, пасиба, буду почитать. =)

    ОтветитьУдалить