27 December, 2008

Очень старый .NET VB compiler bug

Это эту неделю написал несколько классов на C++ для работы с телом .NET IL метода. Данный код жизненно необходим в новом dotTrace для line-by-line & coverage profiling. Прошло 1.5 года с того момента, когда я нашел несоответствие кода порождаемого .NET VB compiler спецификации Ecma-335, Partition II (Metadata Definition and Semantics), 25.4.5 (Method data section). После нескольких .NET Framework Service Pack и выхода VS2008 ничего не поменялось - эта bug живет и здравствует.

Суть проблемы в том, что поле DataSize для структур IMAGE_COR_ILMETHOD_SECT_FAT и IMAGE_COR_ILMETHOD_SECT_SMALL содержит длину секции БЕЗ УЧЕТА заголовка секции (4 байта). По спецификации значение должно быть 4+24*n или 4+12*n соответственно.

02 December, 2008

Особенности реализации стандартных API функций в WindowsMobile

MSDN for WindowsMobile 5.0

"... If the thread is making a kernel call, SuspendThread fails. An application might need to repeat the SuspendThread several times for it to succeed. ..."

21 November, 2008

В семье не без урода

.NET Compact Framework есть существо обделенное не только функциональностью, но и интеллектом. Целый день ковырял странный exception о том, что тип не загружен. Мало того, что не говорится какой именно тип не загружен, но и возникает он в станном месте, где по идее ничего криминального нет. Обидно еще то, что отладчик напрочь отказался коннектиться к приложению на симуляторе. Пришлось отлаживать итерационным методом: исправил, запустил, проверил, что сюда дошли и так по кругу...

Расковырял: оказалось, что любой exception в статическом конструкторе подменяется exception о незагрузке типа без указания что же блин произошло там внутри. Простенько и со вкусом. Дальше больше: код ассембли был скомпилирован для .NET Framework v1.0, а запускался он на .NET Compact Framework v3.5. Так вот в коде были случайно использованы классы и методы (из System.dll) отсутствующие на .NET Compact Framework. Естественно при порытке обратиться к отсутствующему классу программу посылали, но как красиво: сборка System, Culture=null, Version= бла_бла_бла, Token=бла_бла_бла, ... НЕ НАЙДЕНА !!!

11 November, 2008

Мудёр

Вчера обнаружил очень приятную особенность компилятора С++ от VisualStudio 2008 для x86 - он автоматически удаляет блок try/catch если во всех вызовах функций между ними (в том числе и дочерних) не может возникнуть exception.

07 November, 2008

How much does Exception Handling cost, really?

Чувак (он же Kevin Frei) толково рассказывает про то как устроен SEH / C++EH мир для x86 и x64. Что, почем и какого дъявола происходит. Тем, кто еще не забыл С++ и еще может читать по слогам ассемблер посвящается...

Короче, установи PowerPoint и тыкай сюда.

06 November, 2008

Новое в Visual Studio 2010

Я за то чтобы языки программирования развивались в сторону упрощения конструкций, удобства использования, ну и естественно не в ущерб скорости. Особенно это касается С++. Давненько тут не было ничего принципиально нового. Так, по мелочи, что-то менялось и все, но в основном полировались старые и создавались новые библиотеки.

И вот наконец-то появилось что-то новенькое и вкусное для меня - С++ lambda. Все это конечно как бы есть в boost и stl, но кто этим пользовался меня поймет: пишется ну очень много никому не нужных букв для простецкого действия.

На всем этом жиждется еще одна новая фича - паралельные вычисления. Об этом складно и долго вещал чувак из MS на PDC2008. Есть две версии паралельных вычислений: для Managed и для С++ разарботчиков. В VS2010 интегрирована поддержка этого дела - сильно модифицированы отладчик и профайлер, ну и компилятор естественно.

От чего вдруг такая забота? На мой взгляд очень просто - 2 или 4 ядра у проца сейчас норма. А в большинстве случаев программа в один момент времени занимает только один процессор. Программерам просто влом писать параллельный код - работы много и шеф не погладит по головке за убитое время, а результат простому глазу особенно не виден. А тут на тебе 1 функция и дело в шляпе. Быстро удобно и думать почти не нужно. Это то о чем так мечтали большевики... Правда не понятно с накладными расходами на это дело. Вот разгребу накопившиеся дела, поставлю VS2010 и попробую...

07 October, 2008

Приколы ARM ассемблера

Конечно глядя с высоты знания можно заметить, что тут все просто. Но после долго сидения на ассемблерах x86 и x64 данный ARM код вызывает легкую дезориентацию.

E3A03116    mov r3, #0x16, 2
E3830901    orr r0, r3, #1, 18
E12FFF1E    bx lr

Первая команда берет 0x16 и циклически сдвигает вправо на 2 бита - резульат 0x80000005 вторая команда берет 1 и сдвигает циклически вправо на 18 бит, потом делает OR с результатом первой команды. Получаем 0x80004005 в r0 (возвращяемое значение функции).

Вот как эта функия выглядит на C++:

HRESULT Class::Func() { return E_FAIL; }

24 September, 2008

Эволюция ошибок

Сегодня было проведены полномаштабные dotTrace-маневры в обстановке приближенной к боевой. Незамедлительно я отметил для себя удивительную закономерность: предыдущая версия частенько кидала "Out of Memory" (эта стандартная болезнь почти всех профайлеров), а новая похоже будет частенько кидать "Low Disk Space"... Не смешно! Как вам размер снапшотика в 36 Гигабайт? А ведь это всего 20% теста... Другими словами, снапшотик в 180 Гигабайт - это наша объективная реальность. Не смешно, два раза! С другой стороны, один снапшот - это один винчестер. На лицо удобство транспортировки...

P.S. А теперь скажем об этом официально.

API из песка и тумана

Кривые ручка отдельного програмиста - это дело его босса, а вот кривые ручки одной из команд MS - это уже дело каждого. Особенно, если кривость увековечена в WinSDK. Итак, приступим. Постоянно ковыряясь в ассемблере x86 и x64 я заметил, что InterlockXXX функции в x86 - это вызовы API, а в x64 - просто команды ассемблера. Ошибкой было бы думать, что это трагическая случайность. И действительно для x64 и IPF в WinSDK по умочанию сделаны специальные intrinsic-мактосы, а про платформу x86 попросту забыли. Ну не совсем забыли конечно - все можно включить, но это требует некоторого умения и сноровки. Переопределение InterlockedExchangePointer и InterlockedCompareExchangePointer сделано не случайно - без оного мы опять получаем вызовы API вместо ассемблерной команды.

#if defined(_M_IX86)
  #include "intrin.h"
 
  #define InterlockedIncrement _InterlockedIncrement
  #define InterlockedDecrement _InterlockedDecrement
  #define InterlockedExchange _InterlockedExchange
  #define InterlockedCompareExchange _InterlockedCompareExchange
 
  #undef InterlockedExchangePointer
  #define InterlockedExchangePointer(T, V) ((PVOID) InterlockedExchange((PLONG) (T), (LONG) (V)))
 
  #undef InterlockedCompareExchangePointer
  #define InterlockedCompareExchangePointer(D, E, C) ((PVOID) InterlockedCompareExchange((PLONG) (D), (LONG) (E), (LONG) (C)))
#endif

Зачем же я все это говорю, если и так и так все работает. Да вы наверно уже обо всем догадались - ассемблерные команды сильно быстрее. На моем приложении прирост на пустом месте составил более 5%.

27 August, 2008

В продолжение "Ох уж эти ученые мужички"

Сама статья тут

Разберем сначала CAS (compare-and-swap). Как вы знаете для того чтобы атомарно менять память (в том числе и на много- процессорных/ядерных машинах) в x86 и x64 пердусмотрены специальные ассемблерные команды, для совместимости в виндах они спрятаны в системные API вызовы InterlockedXXX. Так вот, если исключить экзотические или не поддерживаемые всеми платформами (в том числе Windows Mobile) наборы InterlockedXXX инструкций (например InterlockedCompare64ExchangeAcquire128 или InterlockedAdd64), то у нас практически остается только работа с указателем через InterlockedCompareExchangePointer и InterlockedIncrement/InterlockedDecrement. В статье же автор умудряется атомарно сравнивать структуры. Конечно теоритически все это возможно, но практического смысла не имеет никакого.

Теперь собственно по алгоритму. Первое что бросается в глаза - это реализация метора Lookup. Мне показалось странным, что инкрементируется счетчик в одной структуре, а декрементируется потенциально в другой. Это немедленно приведет к разъезжанию счетчиков и невозможности правильно удалить старые данные. Единственным рабочим вариантом мне кажется является запоминание того указателя для которого производился инкремент, чтобы потом для него же сделать декремент.

Дальше больше. Метод Update до момента CAS обязан точтно таким же образом инкрементировать счетчик того указателя для которого производится модификация данных. Зачем? Да затем, что неизвестно еще будут ли эти данные отброшены (полный зквивалент Lookup) или нет.

А вот собственно отлаженный и реально работающий код:

template 
struct LockFreeValue
{
private:
    struct Data
    {
        Value m_Value;
        LONG volatile m_Ref;
 
        __forceinline Data() : m_Ref(0) {}
    };
 
public:
    __forceinline LockFreeValue() : m_pData(NULL) { m_pData = new Data();}
    __forceinline ~LockFreeValue() { delete m_pData; }
 
    struct ReadContext
    {
    private:
        LONG volatile * m_pRef;
 
        friend struct LockFreeValue;
    };
 
    __forceinline Value const * EnterRead(ReadContext & context) const // Thread safe
    {
        while (true)
        {
            Data * const pData = m_pData;
 
            InterlockedIncrement(CAST_INTERLOCKED_PLONG(&pData->m_Ref));
            if (m_pData == pData)
            {
                context.m_pRef = &pData->m_Ref;
                return &pData->m_Value;
            }
            InterlockedDecrement(CAST_INTERLOCKED_PLONG(&pData->m_Ref));
        }
    }
 
    __forceinline void LeaveRead(ReadContext const & context) const // Thread safe
    {
        InterlockedDecrement(CAST_INTERLOCKED_PLONG(context.m_pRef));
    }
 
    struct WriteContext
    {
    private:
        Data * m_pOld;
        std::auto_ptr m_New;
 
        friend struct LockFreeValue;
    };
 
    __forceinline Value * EnterWrite(WriteContext & context) const // Thread safe
    {
        while (true)
        {
            Data * const pData = m_pData;
 
            InterlockedIncrement(CAST_INTERLOCKED_PLONG(&pData->m_Ref));
            if (m_pData == pData)
            {
                std::auto_ptr newData(new Data());
                newData->m_Value = pData->m_Value;
 
                context.m_pOld = pData;
                context.m_New = newData;
                return &context.m_New->m_Value;
            }
            InterlockedDecrement(CAST_INTERLOCKED_PLONG(&pData->m_Ref));
        }
    }
 
    __forceinline bool LeaveWrite(WriteContext & context) // Thread safe
    {
        bool const done = InterlockedCompareExchangePointer(
            CAST_INTERLOCKED_PPVOID(reinterpret_cast(&m_pData)),
            context.m_New.get(),
            context.m_pOld) == context.m_pOld;
       
        InterlockedDecrement(CAST_INTERLOCKED_PLONG(&context.m_pOld->m_Ref));
 
        if (done)
        {
            context.m_New.release();
 
            std::auto_ptr oldData(context.m_pOld);
 
            while (oldData->m_Ref)
                SafeSleep(10);
        }
 
        return done;
    }
    __forceinline Value * operator->() { return &m_pData->m_Value; }
    __forceinline Value const * operator->() const { return &m_pData->m_Value; }
 
private:
    Data * volatile m_pData;
};

18 August, 2008

Ох уж эти ученые мужички

Прочитал статью про "Lock Free Data Structures" мистера Andrei Alexandrescu. Подумал, что раз автор решил официально опубликовать это, то он хотя бы проверил то о чем в статье написал. Взял пример из нее попробовал - не работает. Начал вникать - а ЭТО работать не может в принципе вообще никак.

Кстати огромный полновесный камень мистеру Krzysztof Machelski, который получил большие спасибки за то, что проверял этот код и нашел всего 2 ошибки в нём.

P.S. В тему: "я тебе отдалась при луне, а ты взял мои белые груди и связал их узлом на спине - вот и верь после этого людям..."

06 August, 2008

О пользе исходников

Давеча писал новую собиралку .NET метаданных для моего dotTrace'ика. Так вот после 2-х дневного ползанья по исходникам .NET и рытья в отладчике я с удивлением узнал, что:

  1. IMetaDataTables/IMetaDataTables2 интерфейсы хотя и заявлены (отвечают на QueryInterface или отдаются ICorProfilerInfo::GetModuleMetaData), но по факту профайлирным API от Microsoft не поддерживаютя - попытка работы с ними вызывает "Assert Violation".
  2. Для динамических сборок, в отличие от нормальных, используется unmapped PE. Сейчас я понимаю, что это логично. НО, блин, хоть бы где, хоть пол словечка

P.S. Естественно в документации от Microsoft'а ни слова обо всем этом... Так, что исходники - это наше всЁ!!!