пятница, 28 марта 2008 г.

Немного про оптимизацию программ

Всем хорош .Net Framework, но есть у него некоторые особенности, не знание которых гарантированно приводит к падению производительности алгоритмов в два и более раза. Сегодня я расскажу о двух таких особенностях.

1. Debug.Write и Debug.WriteLine

Класс Debug является, пожалуй, одним из самых полезных для более-менее крупных программ. Удобен он тем, что нет необходимости как-то заботиться о сохранении отладочных данных – этим занимается сам дотнет. Главная особенность в том, что если проинициализирован «слушатель» отладочной информации – то эта информация гарантированно будет записана в файл в независимости от того в какой части (и в какой сборке) нашей программы будет вызвана функция записи.

Примерная схема работы с этим классом выглядит так:


            Encoding enc = System.Text.Encoding.GetEncoding(1251);
            FileStream fs = new FileStream("log.txt", FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
            Debug.Listeners.Add(new TextWriterTraceListener(new StreamWriter(fs, enc)));
            Debug.AutoFlush = true;
            …
            …
            Debug.WriteLine(“Эта строка будет записана в файл log.txt”);
Однако, кроме положительных качеств, данный класс имеет и один, но очень большой, недостаток. А именно – при активной записи отладочных данных скорость выполнения программы резко падает. В частности, в моем личном опыте снижение может достигать 50-100% и более.

Как этого избежать?
Создаем вспомогательную переменную типа StringBuilder (я надеюсь, вы уже знаете, что строки складывать в time-critical алгоритмах – нельзя) и в наиболее активно использующих запись отладочной информации функциях пишем в эту переменную. По окончании работы критичной части программы – сбрасываем данные на диск «одним куском».


2. Особенности выделения памяти.

Собственно, про то, как выделяется память – пишут во всех книжках. Но! Никто не упоминает о неприятных последствиях ее выделения.

В частности, худшее, что можно придумать – это выделять память операцией new внутри цикла или нескольких вложенных циклов, а также внутри функций, вызывающихся в цикле. Такой подход гарантированно приводит к падению производительности на 200-300% и более.

Нет, ну понятно, что память выделять все-таки надо :) Но везде, где это возможно, следует вынести выделение памяти за пределы циклов.
В частности, мой переводчик активно работает со строками – это приводит к необходимости постоянно выделять память под все новые и новые строки. Основная оптимизация, которую я начал активно применять – это активное использование заранее созданных StringBuilder’ов.
Идея следующая – при первом вызове функции проверяется на null специальная вспомогательная переменная, являющаяся членом класса (т.е. внешняя по отношению к функции). Если null – вызывается new StringBuilder. В противном случае – вызывается StringBuilder.Clear(), которая очищает содержимое переменной. В этом случае повторное выделение памяти не производится.
С точки зрения всех книг по проектированию ПО – это очень плохо :) Фактически, большинство time-critical функций имеют дополнительно по 1-2 переменным-членам класса. Кроме того – эти функции нельзя использовать в многопоточном режиме. Однако, если конечная цель – обеспечить высокую производительность для однопоточного алгоритма, то такой подход имеет смысл.

Ну и как заключение.

В результате использования только этих двух методов удалось снизить время работы алгоритма перевода на тестовом тексте с 27 секунд до 6 секунд без изменения самого алгоритма. Это примерно 22% прироста. Замечу, что это суммарное время работы всех частей программы – включая и те, что невозможно оптимизировать такими способами. По отдельным функция прирост производительности составлял порядка от 100 до 300% (в основном – за счет избавления от операции new).

2 комментария:

  1. Объясните, что значит падение производительности на 100%...200-300%"? ;)
    Мне казалось, что если производительность упадёт на 100%, то программа просто остановится.

    Евгений.

    ОтветитьУдалить
  2. 100% - это в два раза, 200% - 4 раза. Примерно так...

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