Главная - Литература

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 [193] 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294

25.3. Где искать жир и патоку?

При оптимизации кода вы находите части программы, медленные, как патока зимой, и огромные, как Годзилла, и изменяете их так, чтобы они были быстры, как молния, и могли скрываться в расщелинах между байтами в оперативной памяти. Без профилирования программы вы никогда не сможете с уверенностью сказать, какие фрагменты медленны и огромны, но некоторые операции давно славятся ленью и ожирением, так что вы можете начать исследование именно с них.

Частые причины снижения эффективности

Операции ввода/вывода Один из самых главных источников неэффективности - ненужные операции ввода/вывода. Если объем используемой памяти не играет особой роли, работайте с данными в памяти, а не обращайтесь к диску, БД или сетевому ресурсу

Вот результаты сравнения эффективности случайного доступа к элементам 100-элементного массива «в памяти» и записям аналогичного файла, хранящегося на диске:

Язык

Время обработки внешнего файла

Время обработки данных «в памяти»

Экономия времени

Соотношение быстродействия

6,04

0,000

100%

12,8

0,010

100%

1000:1

Судя по этим результатам, доступ к данным «в памяти» выполняется в 1000 раз быстрее, чем доступ к данным, хранящимся во внешнем файле. В случае моего компилятора С++ время доступа к данным «в памяти» не удалось даже измерить.

Результаты аналогичного тестирования последовательного доступа к данным похожи:

Время обработки Время обработки Экономия Соотношение Язык внешнего файла данных «в памяти» времени быстродействия

С++ 3,29 0,021 99% 150:1

С* 2,60 0,030 99% 85:1

Примечание: при тестировании последовательного доступа данные были в 13 раз более объемными, чем при тестировании случайного доступа, поэтому результаты двух видов тестов сравнивать нельзя.

Если для доступа к внешним данным используется более медленная среда (например, сетевое соединение), разница только увеличивается. При тестировании случайного доступа к данным по сети результаты выглядят так:

Время обработки Время обработки Экономия Язык локального файла файла по сети времени

С++ 6,04 6,64 -10%

С* 12,8 14,1 -10%



Конечно, эти результаты сильно зависят от скорости сети, объема трафика, расстояния между компьютером и сетевым диском, производительности сетевого и локального дисков, фазы Луны и других факторов.

В целом доступ к данным «в памяти» выполняется гораздо быстрее, так что дважды подумайте, прежде чем включать операции ввода/вывода в фрагменты, к быстродействию которых предъявляются повышенные требования.

Замещение страниц Операция, заставляющая ОС заменять страницы памяти, выполняется гораздо медленнее, чем операция, ограниченная одной страницей памяти. Иногда самое простое изменение может принести огромную пользу. Например, один программист обнаружил, что в системе, использующей страницы объемом по 4 кб, следующий цикл инициализации вызывает массу страничных ошибок:

Пример цикла инициализации, вызывающего много страничных ошибок (Java)

for ( column = 0; column < MAX COLUMNS; column++ ) { for ( row = 0; row < MAX ROWS; row++ ) {

table[ row ][ column ] = BlankTableElement();

Это хорошо отформатированный цикл с удачными именами переменных, так в чем же проблема? Проблема в том, что каждая строка (row) массива table содержит около 4000 байт. Если массив включает слишком много строк, то при каждом обращении к новой строке ОС должна будет заменить страницы памяти. В предыдущем фрагменте изменение номера строки, а значит, и подкачка новой страницы с диска выполняются при каждой итерации внутреннего цикла.

Программист реорганизовал цикл:

Пример цикла инициализации, вызывающего немного страничных ошибок (Java)

for ( row = 0; row < MAX ROWS; row++ ) {

for ( column = 0; column < MAX COLUMNS; column++ ) { table[ row ][ column ] = BlankTableElement();

Этот код также вызывает страничную ошибку при каждом изменении номера строки, но это происходит только MAX ROWS раз, а не MAX ROWS * MAX COLUMNS раз.

Степень снижения быстродействия кода из-за замещения страниц во многом зависит от объема памяти. На компьютере с небольшим объемом памяти второй фрагмент кода выполнялся примерно в 1000 раз быстрее, чем первый. При наличии большего объема памяти различие было всего лишь двукратным и было заметно лишь при очень больших значениях MAX ROWS и MAXCOLUMNS.

Системные вызовы Вызовы системных методов часто дороги. Они нередко включают переключение контекста - сохранение состояния программы, восстановление состояния ядра ОС и наоборот. В число системных методов входят методы, служащие для работы с диском, клавиатурой, монитором, принтером и другими



устройствами, методы управления памятью и некоторые вспомогательные методы. Если вас беспокоит производительность, узнайте, насколько дороги системные вызовы в вашей системе. Если они дороги, рассмотрите следующие варианты.

Напишите собственные методы. Иногда функциональность системных методов оказывается избыточной для решения конкретных задач. Заменив низкоуровневые системные методы собственными, вы получите более быстрый и компактный код, лучше соответствующий вашим потребностям.

Избегайте вызовов системных методов.

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

В программе, про оптимизацию которой я рассказал в подразделе «Когда выполнять оптимизацию?» раздела 25.2, использовался кягсс AppTime, производный от коммерческого класса BaseTime (имена изменены). Объекты АррТгте использовались в программе на каждом шагу и исчислялись десятками тысяч. Через несколько месяцев мы обнаружили, что объекты BaseTime инициализировались в конструкторе значением системного времени. В нашей программе системное время не играло никакой роли, а это означало, что мы без надобности генерировали тысячи системных вызовов. Простое переопределение конструктора Knicci BaseTime так, чтобы поле time инициализировалось нулем, дало нам такое же повышение производительности, что и все остальные изменения, вместе взятые.

Интерпретируемые языки При выполнении интерпретируемого кода каждая команда должна быть обработана и преобразована в машинный код, поэтому интерпретируемые языки обычно гораздо медленней компилируемых. Вот примерные результаты сравнения разных языков, полученные мной при работе над этой главой и главой 26 (табл. 25-1):

Табл. 25-1. Относительное быстродействие кода,

написанного на разных языках

Время выполнения кода

Язык

Тип языка

в сравнении с кодом С++

Компилируемый

Visual Basic

Компилируемый

Компилируемый

Java

Байт-код

1,5:1

Интерпретируемый

>100:1

Python

Интерпретируемый

>100:1

Как видите, в плане быстродействия языки С++, Visual Basic и С* примерно одинаковы. Код на Java выполняется несколько медленнее. РНР и Python - интерпретируемые языки, и код, написанный на них, обычно выполняется в 100 и более раз медленнее, чем написанный на С++, Visual Basic, С* или Java. Однако к общим результатам, указанным в этой таблице, следует относиться с осторожностью. Относительная эффективность С++, Visual Basic, С*, Java и других языков во многом зависит от конкретного кода (читая главу 26, вы сами в этом убедитесь).



0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 [193] 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294



0.0024