Arlleex 331 November 27, 2025 Posted November 27, 2025 · Report post Язык Си. Есть такой вот незамысловатый буфер данных, объявленный в файловой области действия: static char buffer[32]; Адрес этого буфера передается функции, которая записывает его в аппаратный регистр DMA-контроллера, который запишет в этот буфер сколько-то байт. Проблема в том, что компилятор не знает о том, что некий DMA чего-то запишет в память. С его точки зрения явных обращений в память буфера нет. А значит - можно очень агрессивно оптимизировать. И уж тем более особенно, когда используется LTO. В целом, в тематических кругах это известная дилемма. Собственно, после завершения DMA-операции чтения в буфер, компилятор должен как-то знать, что несмотря на то, что видимых записей в буфер не производилось, память все-таки была изменена. Ну, во-первых, можно читать ее через указатель на volatile-квалифицированный совместимый тип. Однако это сильно не выгодно - в точке использования volatile компилятор очень сильно заботится о семантике доступа - в частности, старается сохранить ширину доступа на уровне аппаратных инструкций CPU (по крайней мере для базовых типов). Согласно правилам псевдонимизации, к char-буферу, по сути, я могу обращаться только через указатель char *. Разумеется, я не хотел бы читать побайтово буфер, потому что, в конечном счете, буфер - это лишь хранилище для некоторой внешней структуры данных. Соответственно, хотелось читать какие-то части буфера как unsigned int (4 байта), как пример. Не нарушая формальности Си-шного стандарта, сделать это проще всего обычным memcpy(): unsigned int x; memcpy(&x, &buffer[1], sizeof(x)); Только такой способ, а также union-каламбур, позволят корректно работать программе на любом уровне оптимизации и с любыми ключами. И даже больше - только такие способы позволяют развязать компилятору руки в отношении применяемых инструкций доступа к памяти - мой CPU умеет в невыровненный доступ, поэтому здесь он мог бы применить LDR к адресу &buffer[1]. В случае же с побайтовым доступом через volatile char * - это невозможно. Еще один способ - использование __unaligned-квалификатора для указателя: unsigned int __unaligned *ptr = &buffer[1]. Да, это может убедить компилятор в том, что он может использовать инструкцию LDR по невыровненному адресу для данного CPU, но оптимизация чуть более верхнего уровня даже не даст дойти до сюда: такой код содержит UB по нарушению правила псевдонимов указателей. Соответственно, такое использование отклоняется. Хорошо, допустим, остановимся на способе с memcpy() из буфера в локальную переменную. Здесь другая проблема - ничто в данной точке не говорит компилятору о том, изменилась память buffer или нет. Программа не демонстрирует прямого изменения памяти. Как заставить компиятор принудительно поверить, что память конкретно этого буфера действительно была модифицирована? Например, __ASM volatile("":::"memory") заставляет компилятор подумать так обо всех объектах программы, которые косвенно (через указатели-параметры функции) или прямо (как глобальные переменные) используются в месте вставки этого интринсика. Это может быть весьма накладно, особенно когда функция ссылается на десяток различных объектов. Можно ли компилятору дать пинка в отношении только одного конкретного объекта, типа, "Смотри, компилятор, видишь buffer, размером 32 байта? Так вот, компилятор, конкретно сейчас сотри все свои предположения о содержимом этого буфера"? Я не очень хорошо шарю на тему встроенного GCC-синтаксиса ассемблера, поэтому, возможно, кто-то знает. Quote Share this post Link to post Share on other sites More sharing options...
dxp 198 November 28, 2025 Posted November 28, 2025 · Report post Может скажу не то, что вы хотели бы услышать, но, имхо, тут два фундаментальных аспекта C/C++, которые обойти и придумать что-то получше, вроде, пока не выходит. Это работа с асинхронно изменяемыми объектами и необходимость в ручном преобразовании типов. Ваш пример с буфером, заполняемым DMA, -- это один классических примеров асинхронно изменяемого объекта, поэтому тут штатное средство C/C++ -- volatile. Вам не нравится потеря эффективности, и тут на сцену выходит использование объектов других типов с ручным преобразованием типов. К сожалению, здесь есть все те опасности, возникающие из-за отключения контроля типов. Например, ваш буфер компилятор имеет полное право разместить по адресу, кратному sizeof(char), т.е. в большинстве случаев -- 1, что при обращении к этому массиву через объект большего размера даст невыровненный доступ -- а это в лучшем случае потеря производительности (и программисту это в явном виде не показывается, т.е. происходит скрытно -- проц просто работает в этом месте медленнее), в худшем (если CPU не поддерживает такой доступ) -- аппаратное исключение. И тут нужно так же вручную следить, чтобы буфер был размещён правильно. Возможно, более простым и логичным вариантом было бы в таком случае объявить массив другого типа -- того, каким хотите к нему обращаться для эффективности -- массив интов, например, -- тогда volatile ничего не испортит в плане скорости, и массив будет корректно размещён компилятором в памяти. А там где нужны char -- ну там таки да, ручное преобразование типов, но там это скорее всего будет вполне безопасным. P.S. По поводу memcpy я бы не очень на неё надеялся. Встречал утверждения, что, де, "memcpy для вашей платформы сделан самым оптимальным образом в плане производительности", но на практике видел, что там зачастую тупой цикл копирования на уровне байтов. К тому же, компилятор может распознавать имя memcpy и заменять его инлайн циклом. Как и наоборот -- цикл заменяет на вызов memcpy (gcc так точно умеет делать и делает). 1 Quote Share this post Link to post Share on other sites More sharing options...
repstosw 22 November 28, 2025 Posted November 28, 2025 (edited) · Report post Может быть и не в тему, но: 1) Неясны опасения тех, кто боится типкастить указатель одного типа - в указатель другого типа. Очень стандартная вещь. Особенно в парсерах данных, которые загружены в памяти, и не факт, что адреса будут прям выровнены. Почему UB? Результат будет зависеть от настроек кеша данных(был у меня недавно случай, когда чтение из невыровненного слова возвращало мусор - проблема оказалась в политиках кеша) и наличия установленного/сброшенного бита, который разрешает или запрещает доступ к невыровненным данным. Привет вам от x86/x64 или более продвинутых ARM'ов. 2) Касаемо барьеров. Использую барьер памяти, после записи в регистр, который запускает что-то там. Для исключения того, чтобы CPU со своими конвеерами, предсказаниями и кешами не летел вперёд паровоза. Барьер такого вида: __asm__ __volatile__("dsb" : : : "memory"); 3) На агрессивных оптимизациях компилятор может как угодно заменять ваши конструкции (типа копирование байтов ручками) на любые, которые он посчитает нужными. В том числе и на memcpy(), который делит данные на голову, тело и ноги - что позволяет эффективно быстро копировать данные которые невыровнены. Отменить это поеведение можно с помощью специального флага: -force-builtin (ЕМНИП). Если нужно сохранить копирование побайтно ручками, то тут только -O0, -O1 или процедура на ассемблере (LDRB/STRB) Edited November 28, 2025 by repstosw Quote Share this post Link to post Share on other sites More sharing options...
novikovfb 39 November 28, 2025 Posted November 28, 2025 · Report post Что мешает сделать: volatile static int buffer[8]; а при необходимости преобразовывать указатель в char* ? Quote Share this post Link to post Share on other sites More sharing options...
Arlleex 331 November 28, 2025 Posted November 28, 2025 · Report post 3 часа назад, dxp сказал: Возможно, более простым и логичным вариантом было бы в таком случае объявить массив другого типа -- того, каким хотите к нему обращаться для эффективности -- массив интов... Я думал об этом, однако структура, которой я хочу оперировать на уровне объекта верхнего уровня - она весьма разносортная, еще и упакованная. По сути, я нахожусь на уровне стыка формальных правил стандарта языка и фактической реализации в конкретном компиляторе. И я мог бы даже написать тупо unsigned int volatile __unaligned *ptr = &buffer[1]; ... unsigned int const val = *ptr; ... Однако паттерны нарушения правил псевдонимов распознаются компилятором достаточно быстро и, к сожалению, всегда не предсказуемо - на то оно и UB. Забавный факт - из всех случаев (коих был, наверное, десяток за пару лет) ловли UB из-за наличия строгого ключа -f-strict-aliasing запомнился один: вот такой доступ (через указатель на int к объекту char-ов) в одном и том же месте программы компилировался либо в реальный доступ, либо в отсуствие любых инструкций доступа (как не нужный/не возможный по определению) при изменении кода по соседству - т.е. в одном исходном файле, но в разных функциях. Оптимизирующий компилятор знает толк в том, как заставить программистов следовать правилам🙂 Цитата P.S. По поводу memcpy я бы не очень на неё надеялся. Встречал утверждения, что, де, "memcpy для вашей платформы сделан самым оптимальным образом в плане производительности", но на практике видел, что там зачастую тупой цикл копирования на уровне байтов. К тому же, компилятор может распознавать имя memcpy и заменять его инлайн циклом. Как и наоборот -- цикл заменяет на вызов memcpy (gcc так точно умеет делать и делает). Насчет memcpy() достаточно много анализировал сам (смотрел в листинг), и читал комьюнити CLang (у них свой форум есть) - вообще, общее мнение такое: абсолютно не важно, как ты хочешь скопировать байты - вызов memcpy() компилятор постарается превратить во что-то, копирующее быстро и эффективно. И наоборот - писать циклы в ручную на современных оптимизирующих компиляторах (по крайней мере GCC/CLang) почти не имеет смысла - программист не контролирует реальной модели, выбранной компилятором для операции копирования. Этот поведенческий паттерн распознается где-то совсем высоко, и попытки заставить выполняться Си-код именно так, как написан, в общем-то, невозможно. Но вопрос ведь даже не столько в memcpy(), или во что он конкретно развернется (практика наблюдений такова, что на CPU с поддержкой невыровненного доступа int dst; memcpy(&dst, &buffer[1], sizeof(dst)) развернется в одну инструкцию LDR). Вопрос в том, как объяснить компилятору в режиме агрессивной оптимизации, что память конкретного объекта, которую не меняли явным образом (простыми словами, нет в коде явных строчек типа buffer[0] = 10; buffer[x] = y; ...), в действительности поменялась извне. По логике работы мне подходит барьер компилятора __ASM volatile("":::"memory"), но меня не устраивает атрибут memory, т.к. я хочу пнуть компилятору втык, что память у меня меняется не вся, а только конкретного объекта. Остальные объекты он трогать не должен, т.к. согласованный доступ к ним я разруливаю вручную самостоятельно - там как раз через volatile-доступы, но то не буферы, а тупые флаги-переменные, перекидываемые различными ISR/процессами. 2 часа назад, repstosw сказал: 1) Неясны опасения тех, кто боится типкастить указатель одного типа - в указатель другого типа. Очень стандартная вещь. Не любой указатель безопасно типкастить в указатель на другой тип. Что во что можно кастовать - есть определенные правила. Я согласен, писать программы с UB и надеяться на лояльность компилятора (или, когда возникает причинно-зависимый баг, отключают соответствующий ключик оптимизатора) - это весело. Однако это и веселый забег по граблям. При компиляции в GCC не факт, что все то же поведение будет у CLang. И наоборот. Напишете по-правильному, работать будет везде. Цитата Результат будет зависеть от настроек кеша данных(был у меня недавно случай, когда чтение из невыровненного слова возвращало мусор - проблема оказалась в политиках кеша) и наличия установленного/сброшенного бита, который разрешает или запрещает доступ к невыровненным данным. Вы описываете поведение, зависящее от реализации конкретного железа - это вообще отдельная степь. Параллельная дорога кодогенерации. Я не говорю про то, что будет с железом при исполнении конкретных инструкций с некоторыми "неправильными" данными. Я говорю о том, что код на Си, скомпилированный оптимизирующим компилятором, не должен полагаться на UB. Практика надеяться на компилятор, что он сделает то, чего он не обязан по стандарту языка или декларируемыми поведениями в его документации - плохая практика. Цитата 2) Касаемо барьеров. Использую барьер памяти, после записи в регистр, который запускает что-то там. Для исключения того, чтобы CPU со своими конвеерами, предсказаниями и кешами не летел вперёд паровоза. Барьер такого вида: Барьеры памяти != барьеры компилятора/оптимизатора. Первые нужны для синхронизации на уровне flow-control CPU. Конвейер, кэши, шины, и т.д. К кодогенерации компилятора, по-прежнему, это не имеет никакого отношения. Иногда компиляторы гарантируют, что вызов интринсика барьера памяти для CPU неявно порождает и барьер компилятора. Цитата На агрессивных оптимизациях компилятор может как угодно заменять ваши конструкции (типа копирование байтов ручками) на любые, которые он посчитает нужными. В том числе и на memcpy(), который делит данные на голову, тело и ноги - что позволяет эффективно быстро копировать данные которые невыровнены. Отменить это поеведение можно с помощью специального флага: -force-builtin (ЕМНИП). Это мне известно. Я не против - пусть копирует. Как раз весь смысл в том, чтобы развязать ему руки - пусть копирует как хочет, но главное - копирует. Однако, зачем вообще копировать, если в буфер никто не писал, не так ли? Так если никто явно не писал, то вызов int dst; memcpy(&dst, &buffer[1], sizeof(dst)) может всегда заменяться на MOV R0, 0, ведь по стандарту глобальные переменные без инициализации получают значение 0. 1 час назад, novikovfb сказал: Что мешает сделать: volatile static int buffer[8]; а при необходимости преобразовывать указатель в char* ? Нет, это не сработает. 1 Quote Share this post Link to post Share on other sites More sharing options...
Arlleex 331 November 28, 2025 Posted November 28, 2025 · Report post Вот, к примеру, коллеги по несчастью, пишут, что, дескать, есть способ: https://www.eevblog.com/forum/microcontrollers/gcc-compiler-and-optimization/ asm volatile ("": "+m" (*(char (*)[2])buffer)); 2 - это для примера, это размер массива buffer. Если бы еще было понимание, полностью ли это корректно написано, или тут упущен какой-нибудь "memory" и т.д. Quote Share this post Link to post Share on other sites More sharing options...
dxp 198 November 28, 2025 Posted November 28, 2025 · Report post 43 минуты назад, Arlleex сказал: По логике работы мне подходит барьер компилятора __ASM volatile("":::"memory"), но меня не устраивает атрибут memory, т.к. я хочу пнуть компилятору втык, что память у меня меняется не вся, а только конкретного объекта. Ну, по сути-то вы хотите указать компилятору, что у вас асинхронно изменяемый объект, и volatile для это подходит напрямую -- оно именно про это. Вам не нравится, что при этом возникает "побочный эффект" в отключенной оптимизации. Как побороться за скорость, тут вам виднее, я не понял, почему объявить массив как массив интов, не подходит, если всё равно руками кастовать типы. 5 минут назад, Arlleex сказал: Если бы еще было понимание, полностью ли это корректно написано, или тут упущен какой-нибудь "memory" и т.д. Ну, это вообще непереносимая штука, но я понял, это вам не мешает в данном случае. И гарантии работоспособности тут кто даст? Если только в доках найти описание, а иначе это как использование недокументированных особенностей компилятора, а они сегодня есть, завтра нет. Ну, выглядит не сказать, чтобы красиво, костыльно, имхо. 1 Quote Share this post Link to post Share on other sites More sharing options...
Сергей Борщ 181 November 28, 2025 Posted November 28, 2025 · Report post 1 час назад, Arlleex сказал: Однако, зачем вообще копировать, если в буфер никто не писал, не так ли? dxp уже дважды писал про volatile, я, на всякий случай, еще раз обращу на него внимание. Quote Share this post Link to post Share on other sites More sharing options...
Arlleex 331 November 28, 2025 Posted November 28, 2025 · Report post 17 минут назад, dxp сказал: Ну, по сути-то вы хотите указать компилятору, что у вас асинхронно изменяемый объект, и volatile для это подходит напрямую -- оно именно про это. Вам не нравится, что при этом возникает "побочный эффект" в отключенной оптимизации. Как побороться за скорость, тут вам виднее, я не понял, почему объявить массив как массив интов, не подходит, если всё равно руками кастовать типы. Да, хотел сказать, что объект изменяется "силами, не подвластным пониманию компилятора". С volatile, конечно, вопрос решаемый - но гарантированно корректно это будет работать при побайтовом копировании. Я, разумеется, могу смириться с неоптимальностью такого решения, просто думал, что есть 100% способы пометить объект как изменяемый асинхронно, но не применяя volatile. Как раз с целью подсказать компилятору "действовать как обычно" - не ходить как прикажут по байту, а так как ему удобно эти самые байты мне доставить. Главное - доставить. Вторичной надеждой, конечно, как и в большинстве случаев, я предполагаю, что он не дурак и будет по полной юзать нормальные словные загрузки из памяти, потому что знает, что CPU их умеет. 17 минут назад, dxp сказал: Ну, это вообще непереносимая штука, но я понял, это вам не мешает в данном случае. И гарантии работоспособности тут кто даст? Если только в доках найти описание, а иначе это как использование недокументированных особенностей компилятора, а они сегодня есть, завтра нет. Ну, выглядит не сказать, чтобы красиво, костыльно, имхо. К сожалению, это один из тех самых непереносимых способов, но вопрос гарантий для меня здесь как раз в активном поиске. Если наличие гарантий 100%, то я смогу это применить, т.к. меня интересует, в общем-то, GCC и CLang. 9 минут назад, Сергей Борщ сказал: dxp уже дважды писал про volatile, я, на всякий случай, еще раз обращу на него внимание. Я, разумеется, помню про volatile и зачем и в каких случаях он требуется. Однако я повторюсь, что мой изначальный посыл в попытке разрешить оптимизированный доступ к памяти (в части свободы выбора конкретных инструкций) вопреки ряду других препятствий. В конце концов, возможно действительно стоит объявить буфер как массив из элементов, доступ к которым я хочу ожидать максимально эффективным - u32 (4 байта). При обращении квалифицировать локальный указатель как указатель на volatile u32 *. А уже потом врукопашную распихивать по упакованным членам структуры, если требуется. Quote Share this post Link to post Share on other sites More sharing options...
jcxz 359 November 28, 2025 Posted November 28, 2025 · Report post 5 часов назад, dxp сказал: Например, ваш буфер компилятор имеет полное право разместить по адресу, кратному sizeof(char) Не все компиляторы. Например IAR for ARM по умолчанию разместит такой массив гарантированно по адресу, кратному 4. И чтобы он так не делал, надо дать ему спец. ключ: --no_var_align 4 часа назад, repstosw сказал: Касаемо барьеров. Использую барьер памяти, после записи в регистр, который запускает что-то там. Аналогично. Кроме того, ставлю барьер после записи в регистр периферии, находящийся на одном сегменте матрицы шин, если после этой записи идёт работа с регистрами другой периферии, находящимися на другом участке матрицы шин. Если состояние или работа тех регистров (на 2-м шинном сегменте) зависит от содержимого регистра на 1-м шинном сегменте. Вобщем: ставлю барьер между участками инита периферийных регистров, находящихся на разных шинных сегментах. Если между этими регистрами есть какая-то связь по функционированию. Quote Share this post Link to post Share on other sites More sharing options...
jcxz 359 November 28, 2025 Posted November 28, 2025 · Report post 3 часа назад, Arlleex сказал: По логике работы мне подходит барьер компилятора __ASM volatile("":::"memory"), но меня не устраивает атрибут memory, т.к. я хочу пнуть компилятору втык, что память у меня меняется не вся, а только конкретного объекта. Остальные объекты он трогать не должен, т.к. согласованный доступ к ним я разруливаю вручную самостоятельно Меня это тоже напрягало не раз. И тоже задумывался как тут быть. Но глубоко не копал. Времени как всегда нет. Как я понимаю: вы хотите с char buffer[32] работать через DMA, а после (или до) - обращаться к этой памяти как если бы там лежала структура? (упакованная к тому же) Тогда как гипотеза: А если попробовать сделать union, в котором char buffer[32] будет с volatile (и DMA использует её) и + наложенная на него структура, с полями которой работает CPU - без volatile. И по границам участка доступа к членам структуры, расположить доступы к buffer[x] (фиктивные, чтение)? Может это заставит компилятор быть более сдержанным? Смысл здесь в том, что обычно компилятор имеет право переносить оптимизированные доступы через volatile доступы. Но в таком случае, видя что оптимизированные доступы идут к тому-же же объекту, что и volatile-доступы (видя прям в этом же участке кода), он возможно не станет их переносить через volatile-доступы. А на работу с остальными переменными это никак не повлияет. Но это как гипотеза только! Вообще - у CCS был (есть?) модификатор restrict для указателей. Который говорил компилятору "у данной памяти есть side effects за пределами данного кода (функции) где этот указатель используется". И оптимизацию нужно проводить осторожнее. Имхо - restrict там менее строг, чем volatile. Жаль что у других компиляторов такого нет. Quote Share this post Link to post Share on other sites More sharing options...
Arlleex 331 November 28, 2025 Posted November 28, 2025 · Report post 16 минут назад, jcxz сказал: Как я понимаю: вы хотите с char buffer[32] работать через DMA, а после (или до) - обращаться к этой памяти как если бы там лежала структура? (упакованная к тому же) По сути - да. Только в реальности буфер - не один, их целый пул. И конкретный из них (свободный) выдается как только потребуется, для обслуживания DMA в него. Цитата Тогда как гипотеза: А если попробовать сделать union, в котором char buffer[32] будет volatile (и DMA использует её) и + наложенная на него структура, с полями которой работает CPU - без volatile. И по границам участка доступа к членам структуры, расположить доступы к buffer[x] (фиктивные, чтение)? Может это заставит компилятор быть более сдержанным? Union здесь хорош только в качестве возможности доступа к некоторым полям структуры напрямую, не задумываясь о выравниваниях и т.д. Сам буфер смысла делать volatile особого нет, ведь его адрес всего лишь будет рано или поздно записан в аппаратный регистр DMA-контроллера. А вот сделать именно структуру как volatile - тут можно подумать. Quote Share this post Link to post Share on other sites More sharing options...
jcxz 359 November 28, 2025 Posted November 28, 2025 · Report post 16 минут назад, Arlleex сказал: Сам буфер смысла делать volatile особого нет, ведь его адрес всего лишь будет рано или поздно записан в аппаратный регистр DMA-контроллера. А вот сделать именно структуру как volatile - тут можно подумать. Я имел в виду: union { char volatile buffer[32]; struct { ... } data; } static; Если конечно все компиляторы позволяют такое... не уверен... И по границам участка кода, работающего с data, расположить два фиктивных обращения к buffer. 2 лишних volatile-обращения - имхо небольшая плата за возможность работы внутри с data оптимизированным образом. Особенно если data - достаточно большая. Quote Share this post Link to post Share on other sites More sharing options...
Arlleex 331 November 28, 2025 Posted November 28, 2025 · Report post 24 минуты назад, jcxz сказал: Я имел в виду: union { char volatile buffer[32]; struct { ... } data; } static; Если конечно все компиляторы позволяют такое... не уверен... И по границам участка кода, работающего с data, расположить два фиктивных обращения к buffer. 2 лишних volatile-обращения - имхо небольшая плата за возможность работы внутри с data оптимизированным образом. Особенно если data - достаточно большая. Я понял о чем вы говорите, только здесь есть один тонкий нюанс, который тоже будет зависеть от реализации: с точки зрения объектной модели, члены buffer и data - это разные объекты, хоть и перекрывают физическую память друг друга. Поэтому запись buffer, а чтение data ничем не отличается от моего исходного варианта, даже если обложить фиктивными доступами на чтение к buffer доступ к data. Если возможно будет доказать, что доступ к buffer не влияет на изменение data, то компилятор по-прежнему может не читать data, считая его "итак известным". Это если опираться на формальные правила. Что обещает (и самое главное - обещает ли?) конкретный компилятор в этом случае не получится, если только сами разработчики его не скажут. Quote Share this post Link to post Share on other sites More sharing options...
jcxz 359 November 28, 2025 Posted November 28, 2025 · Report post 1 час назад, Arlleex сказал: с точки зрения объектной модели, члены buffer и data - это разные объекты Разве? Имхо - компилятор должен их считать единым объектом. С разными режимами доступа. Ну или пускай двумя объектами, но с side effects между ними. И ограничить уровень оптимизации, только локальной оптимизацией. Я к чему говорил про restrict в CCS: Если в CCS в коде используются 2 разных указателя без restrict, то предполагается, что они могут адресовать перекрывающиеся области памяти. И соответственно - любые оптимизации работы по ним должны быть только такими, которые учитывают такую вероятность. Если же указателям дать модификатор restrict, то предполагается, что они адресуют регионы памяти, гарантированно не пересекающиеся и не влияющие друг на друга (no side effects). А значит - для них возможна максимальная оптимизация. Вот этим union с двумя разными объектами внутри я попытался добиться чего-то подобного: Чтобы компилятор ограничивал уровень оптимизации, подобно CCS для указателей без restrict. Quote Share this post Link to post Share on other sites More sharing options...