singlepost

Глупые Ошибки! или к чему приводит оптимизация! << На главную или назад  

Это так, не вопрос.. а просто "Крик души…" как не надо делать!
Может кому пригодиться ;)

Написал небольшой кусочек кода, что то типа простейшего менеджера памяти.Многие могут возразить "зачем изобретать велосипед и писать свой менеджер памяти?"

Отвечаю: необходимо было выделить достаточный объем непрерывной
физической памяти в которую складировать некие события, с той целью что если во время сбора данных от устройства, произойдет BSOD – то после того как система сдампит память на диск я смогу найти свой непрерывно выделенный буфер в файле и анализировать его, увидеть последние события которые обрабатывало устройство в тот момент. Ну в общем все по образу и подобию работы утилитки DebugView.

Так вот. Алгоритм довольно простой. Во время инициализации выделяю кусок физической памяти и в нем по мере необходимости выделяю более мелкие блочки для сохранение потока данных! Т.к запросы на выделение памяти могут прийти в произвольный момент и от произвольного контекста,
необходимо было добавить синхронизацию! Т.к вся эта обработка происходит в Kernel-mode то на ум сразу пришло использовать SpinLock!

!!!! И вот тут сыграло мое стремление заоптимизить кусочек кода.

Зачем использовать SpinLock и синхронизироваться посредством хард-процессорной синхронизации, когда можно использовать софтварный Mutex, что будет более приемлемо и на многопроцессорной машине не будет переводить проц в бесполезный цикл, если в этот момент SpinLock был захвачен другим процом.

Вот я и заложил БОМБУ замедленного действия в свой код. Mutex он конечно хороший объект синхронизации, но для user-mode. В kernel-mode есть некие ограничения! И эти ограничения вылетели у меня в тот момент из головы. Заглянуть в MSDN тоже не сообразил. Написал код – запустил. Все отлично работает…

работает…
работает… пару минут… и…

И в произвольный момент времени система умирает… Вернее не вся система в целом, а лишь потоки user-mode перестают получать возможность выполняться и вытесняются потоками kernel-mode.

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

ExAcquireFastMutex(&g_DeviceExtension->g_drMutex);

Count++;

ExReleaseFastMutex(&g_DeviceExtension->g_drMutex);

Который как видно он ничего не делает, кроме как безопасно инкрементирует переменную!

и тут пришло озарение… Оптимизация Блин.. хотел сделать лучше… а вышло…

Дело оказалось в том, что система опрашивала устройство в тот момент когда захватывать Mutex нельзя ни при каких условиях IRQL поднимался до DISPATCH_LEVEL, это в итоге приводило к тому, что user-mode потоки не могли получить шанс на выполнение, в виду того, что скорей всего не была завершена синхронная операция выполняющаяся
на уровне между PASSIVE_LEVEL < … < DISPATCH_LEVEL.

Вот… переписав код и поменяв Mutex на Spinlock все работает прекрасно!

Так что иногда можно даже в логически правильном коде сделать грубейшую ошибку, выловить которую будет очень трудно!

Всем удачи!

68 ответов в теме “Глупые Ошибки! или к чему приводит оптимизация!”

  1. 11
    Александр Lert ответил:

    Эт про 5% и пряморуких программистов я не знаю :) Конечно, когда надо делать уйму вычислений, то ООП может навредить. Но я не говорил про случаи, когда каждый такт процессора важен – тогда надо на асме писать :) Я просто говорю, что всегда лучше не полагаться на какие-то слухи о тормозах, а самому проверить и выяснить, есть тормоза или нет, то есть то самое профилирование.

    А для kernel и вправду интересно писать :) Но это кому как, конечно.

  2. 10
    Cyber Max ответил:

    > встречаем 5% падения производительности у самых
    > пряморуких программистов.

    Тут кстати очень поможет профилировщик.. покажет узкие места, проанализировав которые можно будет переписать кусочки кода которые понижают производительность!

    > А вообще написание чего угодно для кернел мода
    > виндоус – это лучше сразу написать новую систему и не мучаться.

    Как по мне намного интересней писать для Kernel-mode, чем для user-mode:
    ответственности больше…и код в режиме ядра выглядит как то более благородней.. структурирований что-ли..

  3. 9
    Филипп Полковников ответил:

    Тысячные доли общего времени исполнения мы получаем, если ООП отвечает за всю железную дорогу Украины. А когда ООП начинают использовать для виртуальных объектов, а особенно для низкоуровневых, а особенно не зная сути применения ООП, именно тогда и получаются висячий и нечитабельный софт.

    Запомните – ООП применяется если объект /видно/. Если это чашки/тарелки/яблоки. Операций над ними производится мало, а внутри функциональности много. А уже используя классы для полиномов, над которыми надо оперировать часто, или над длинными числами, сразу встречаем 5% падения производительности у самых пряморуких программистов.

  4. 8
    Александр Lert ответил:

    Да, я наконец понял, что вы имеете в виду. Спасибо.

    А собственно про ООП: вы указали на обычные его недостатки. Но создание/удаление объекта – это ведь не какие-то особенные операции. А их всегда боятся :) и говорят, что на них тратится время. Конечно, затраты есть. Но рекомендую: один раз подсчитать (замерить), сколько тысячных долей процента времени выполнения программа проводит в операциях создания/удаления, и навсегда забыть об этих страхах :)
    Конечно, надо правильно проектировать. Если вы будете передавать все объекты по значению, и конструктор копирования будет делать уйму операций, то это будут большие тормоза. Но это уже не дефект ООП, а дефект в голове проектировщика.

    Но что-то я ушел от темы :)

  5. 7
    Cyber Max ответил:

    При использовании мьютекса приостановиться поток (если мьютекс захвачен) .. переключиться контекст и проц (в многопроцессорной системе) будет выполнять другую полезную работу… если будет спин-лок – то проц будет тупо ожидать освобождения… тут термин "Оптимизация" имелось в виду – дружественного использования микропроцессоров, а не скорости выполнения….

    А вот использование ООП… идея конечно классная.. но как по мне.. будет расходоваться время на создание объекта – вызов конструктора и разрушение – вызов диструктора… (хотя если объект выделяется на стеке.. то значительно быстрее), вместо обычного вызова функций Захвата и Освобождения объекта синхронизации.. но это чисто мое мнение :)

  6. 6
    Александр Lert ответил:

    2 #6: Какое-то разногласие у нас :) По моим данным, спинлок всегда быстрее, чем мьютекс, хоть на однопроцессорной, хоть на многопроцессорной системе. Единственно, когда он хуже – если предполагается делать блокировку на более-менее длительное время, и то не из-за скорости, а потому, что процессор не занимается полезным делом. Мьютекс же приостанавливает поток, но приостановка/возобновление потока – более медленные операции, чем спин-блокировка. Разве нет? Чем все-таки мьютекс более приемлемый?

    2 #5: Неее, самые трудно отлавливаемые баги – с синхронизацией :)

    Кстати, для синхронизации очень удобно использовать ООП. Например, специальный класс выполняет захват/освобождение ресурса при входе/выходе в область видимости (делая это в конструкторе/деструкторе). Таким образом, "забыть" освободить ресурс невозможно даже при наличии множественных return'ов или активной работе с исключениями C++. Вообще, весь код синхронизации сводится по сути к объявлению объекта в начале функции, вот и всё.
    Даже один этот класс упрощает работу очень существенно, а можно сделать и другие. С ними вообще багов в синхронизации почти нет. Рекомендую :)

    Правда, всё это относится только к user-time. В случае драйверов использовать C++ не рекомендуется, а жаль…

  7. 5
    Cyber Max ответил:

    to #4
    Сount++ там оставлен для примера.. на самом деле там выполнялась работа по резервированию кусочка памяти в уже зарезервированном физическом пространстве.

    > В любом случае, у вас неправильно написано На
    > однопроцессорной машине
    > spinlock не "переводит процессор в бесполезный цикл"

    Помоему сейчас будет просто не вежливо писать код для однопроцессорных систем… поэтому сразу акцент был сделан на многопроцессорную машину.
    Если бы не эта ситуация в которой запрос к защищенному блоку иногда приходит на уровне DISPATH_LEVEL – то как раз FAST_MUTEX был бы более приемлимым чем Spinlock… Всетаки не стоит везде использовать SpinLock…

    Даже при обработке Cancel Irp всегда стараюсь как можно быстрее освободить CancelSpinLock – чтобы не деградировать систему!

  8. 4
    Антон Щиров ответил:

    По статистике самые трудно отловляемые баги – самые глупые.

    Сегодня два часа убил на поиск дедлока потоков – оказалось вызывался лишний ReleaseSemaphore()

  9. 3
    Александр Lert ответил:

    Почему бы для оптимизации count++ не использовать InterlockedIncrement – это наискорейший способ, не зависящий от IRQL :) Причем функции семейства Interlocked* есть как в user-mode, так и в kernel-mode. Это гораздо лучше, чем даже spinlock :)
    Конечно, если действительно нужно только и всего, что count++.

    В любом случае, у вас неправильно написано. На однопроцессорной машине spinlock не "переводит процессор в бесполезный цикл" – на самом деле в однопроцессорной версии ядра при захвате спинлока только повышается IRQL до DISPATCH_LEVEL, и ничего более! Поэтому, на самом деле вы сначала задумали "пессимизацию", так как мьютексы, даже быстрые (FastMutex), ВСЕГДА работают медленнее, чем спинлоки.

  10. 2
    Константин Смотритель ответил:

    Это скорее не оптимизация, а неучтённый побочный эффект. И IRQL здесь – как глобадьная переменная…

  11. 1
    Филипп Полковников ответил:

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

Клуб программистов работает уже ой-ой-ой сколько, а если поточнее, то с 2007 года.