Главная страница Visual 2000 · Общий список статей

Поговорим о программировании
Размышления бывшего программиста
Часть 4

Андрей Колесов

© Андрей Колесов, 2001
Авторский вариант. Статья была опубликована c незначительной литературной правкой в "КомпьютерПресс" N 04/2001, CD-ROM.

Весь сериал

ПРИЛОЖЕНИЕ (отдельный файл): Разбор полетов


Эффективность программного кода — показатель квалификации разработчика

Говоря в предыдущей части об отличительных чертах хорошей программы, я поставил эффективность кода на четвертое место после соответствия спецификациям, сроков исполнения и простоты сопровождения. Но еще раз подчеркну, что, несмотря на стремительный рост вычислительных мощностей компьютеров, эффективность кода (здесь два основных показателя — скорость выполнения программы и требуемый объем памяти) является крайне важной. И в любом случае — это наиболее очевидный показатель квалификации программиста. Ведь проверка специалиста, например, на уживчивость в коллективе и способность выполнять задания в срок требует времени, а неумение писать компактный код сразу видно невооруженным (хотя и опытным) глазом.

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

Хотел бы еще раз подчеркнуть мысль, высказанную в прошлый раз: создание эффективного кода в целом не противоречит требованиям снижения трудозатрат. Сошлюсь на собственный опыт, который показывает, что внимательное "вылизывание" своего собственного кода первого работоспособного варианта программы почти всегда позволяет повысить его эффективность на 20-50%. В действительности это не так много, и такой повторный анализ программ целесообразен только для критически важных приложений (например, для коробочного продукта это может быть очень важным).

Но проблема заключается в том, что слишком часто встречаются примеры реальных программ, неэффективность которых сразу же бросается в глаза, и повышение, например, быстродействия которых в 2-3 раза обеспечивается весьма простыми приемами.

В связи с этим вспоминается общение с одним программистом, ставшее одной из побудительных причин начала "Размышлений" (см. приложение "Разбор полетов").

В начало статьи

Несколько советов по поводу повышения эффективности программ

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

Вопросы эффективности, по моему мнению, можно достаточно условно разделить на несколько уровней:

  1. Разработка алгоритмов на уровне вычислительной математики.
  2. Реализация этих алгоритмов с учетом особенностей цифровых вычислительных машин.
  3. Учет особенностей конкретного языка программирования.
  4. Управление обменом данными с внешней памятью вообще и применение СУБД в частности.

В начало статьи

Вычислительные алгоритмы

Проще говоря, нужно выполнять оптимизацию вычислительных или логических конструкций. В тривиальном примере это означает, что вместо:


y = (d * x) + (x * c) 

нужно писать:


y = x * (d + c).

Хотя в такой алгебраической записи преобразование представляется совершенно очевидным, в программе его довольно часто не замечают. Особое внимание следует уделять вычислениям в цикле, минимизируя повторные операции. Например, конструкцию:


A = 0
For i = 1 To N  
  A = A + Sin(x) * i  
Next  

нужно заменить на:


A = 0: y = Sin(x)
For i = 1 To N  
  A = A + y * i  
Next  

Еще лучше на:


A =0
For i = 1 To N
  A = A + i  
Next
A = A * Sin(x)

А еще лучше вспомнить формулу суммы арифметической последовательности и написать:


A = Sin(x) * N * (N + 1) / 2

То же самое относится к упрощению логических конструкций:


If v0 And v1 Then ... 
If v0 And v2 Then ...  
If v0 And v3 Then  

лучше заменить на:


If v0 Then
  If v1 Then ...  
  If v2 Then ...  
  If v3 Then ...  
End If  

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


If v0 And v1 Then... 

и при этом вы знаете, что v1 в 90% случаев имеет значение False. В этом случае имеет смысл написать по-другому:


If v1 Then 
  If v0 Then...  
End If  

или записать в такой последовательности, если вы знаете, что компилятор сам выполняет оптимизацию вычислений:


If v1 And v0 Then...

Подобные соображения необходимо учитывать и тогда, когда применяется конструкция типа Select. Например, вы пишете код для какой-то обработки прописных и строчных латинских букв:


Select Case AnsiCode 
  Case 65 To 90   ' прописные (верхний регистр)  
  Case 97 To 122  ' строчные  
End Select  

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

В начало статьи

Учет машинной реализации

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

Собственно, программирование — это процедура формулировки абстрактных математических алгоритмов в компьютерных терминах. В основном она заключается в правильном выборе типов данных и операций их обработки. Само наличие в системах программирования разных типов данных вызвано необходимостью оптимизации программного кода (иначе вместо всех числовых данных мы бы просто применяли один формат, например вещественный с двойной точностью).

Здесь всегда необходимо помнить об очень простых вещах:

  1. Операции с целочисленными данными всегда выполняются быстрее (намного!), чем с вещественными.
  2. Все арифметические операции имеют разное время выполнения. В общем случае последовательность в порядке возрастания такова: логические операции, сложение и вычитание, умножение и, наконец, деление.
  3. Чем короче длина переменной, тем быстрее она обрабатывается (не говоря уже об экономии памяти!). Обработка простых типов данных всегда требует меньше времени, чем при использовании сложных структур и массивов.
  4. Следует избегать излишних преобразований типов данных.
  5. Операции вызова процедур являются довольно длинными с точки зрения времени выполнения. Это не значит, что их следует избегать, просто использовать их нужно внимательно.

Список этих советов можно продолжить, но пока я просто приведу несколько фрагментов кода. Например, такие конструкции, как:


A = 0
For i = 1 To N
  A = A + i  
Next
A = A * Sin(x)
или

A = Sin(x) * N * (N + 1) / 2  

лучше записать в следующем виде, указав тип переменных в явном виде (в предыдущем примере по умолчанию использовался Variant):


Dim Ai As Integer, i As Integer, A As Single 
Ai = 0   
For i = 1 To N
  A = A + i
Next
A = Ai * Sin(x)   
или


Ai =  N * (N + 1) / 2
A = Sin(x) * Ai

В общем случае вариант:


A = 2.* Sin(x)

всегда будет работать быстрее, чем:


A = 2 * Sin(x)

В последнем случае каждый раз выполняется преобразование целочисленной константы в вещественный вид.

Как всегда, особое внимание следует уделять повторяющимся конструкциям, в том числе циклам. Например, если у вас имеется такой вариант обращения процедуры:


For i = 1 To Ni
  For j = 1 To Nj  
    Call MyProcedur (MyArray (i, j))  
  Next  
Next  

то явно стоит подумать о написании другого варианта процедуры:


Call MyProcedur (MyArray())

перенеся туда вложенные циклы.

В начало статьи

Учет специфики языка программирования

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

Например, результаты одного подобного исследования (во времена MS-DOS) показали, что скорость работы Fortran-программы была в несколько раз выше по сравнению программой, написанной на QuickBasic. Но авторы текста забыли (или не знали), что по умолчанию Fortran использует статические переменные, а QB — динамические. Стоило лишь вставить в QB-программы ключевое слово Static, и по быстродействию программы практически сравнялись бы...

В начало статьи

Управление обменом данными с внешней памятью и СУБД

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

Что касается работы с СУБД, то это отдельная огромная тема. Если дефекты написания обычных вычислений могут привести к разбросу производительности в несколько раз, то неоптимальность структуры базы данных и алгоритмов взаимодействия в нем приводит к разрыву в производительности в десятки и сотни раз.

В начало статьи

ПРИЛОЖЕНИЕ (отдельный файл): Разбор полетов