Тестирование, отладка и оптимизация

(В литературе часто этот этап просто называют тестированием). На этом этапе производится всесторонняя проверка программ. По поводу того как должно выполняться тестирование, среди разных программистов нет еденного мнения, но они единодушны в том, как тестирование не должно выполняться. Тестирование программы (или ее отдельных модулей) не должен выполнять программист (или группа программистов), создавший эту программу (модуль).

Рассмотрим этот этап более подробно. Существуют три аспекта проверки программы на:

- правильность;

- эффективность реализации;

- вычислительную сложность.

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

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

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

Тестирование, отладка и оптимизация - student2.ru

Из диаграммы видно, что большинство ошибок совершаются именно на этапе кодирования.

Существуют два типа программных ошибок: синтаксические и семантические. Синтаксические ошибки возникают из-за нарушений правил (лексики) языка программирования и выявляются во время компиляции. Такие ошибки могут быть исключены сравнительно легко. Большинство из них можно выявить путем простого просмотра текста программы. Чаще всего программисты оставляют этот этап компилятору, однако сквозной просмотр текста иногда бывает полезен. Скорость исправления ошибок, выявленных компилятора зависит от степени знакомства программиста с данным языком. Семантические или логические ошибки приводят к некорректным вычислениям или ошибкам во время выполнения программы (run-time error). Математическая безупречность алгоритма не гарантирует правильности его перевода в программу. Аналогично разумный вид получаемых результатов не дает достаточной гарантии правильности программы. В общем случае нельзя дать общего решения для проведения проверки на правильность программы. Конечно, самым лучшим способом тестирования программы является поставка ее пользователю сразу же после завершения программирования. Единственное слабое место такого метода – этот пользователь больше никогда не купит у вас не одной программы. Выявление и устранение ошибок часто имеет циклический характер. Устранение одной ошибки может породить другую ошибку.Особенно это касается работы с глобальными переменными.

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

1. Чем раньше спроектирован тест, тем вероятнее выявление ошибок. Поэтому лучше готовить тесты еще на этапе проектирования системы.

2. Недопустима хаотичность процесса тестирования. Он должен быть документирован и полностью управляем.

3. Необходимы повторяемость и завершенность тестов.

4. Следует избегать добавления новых тестов в процессе тестирования.

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

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

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

Очень часто тесты (файлы с тестовыми данными) создаются вручную. Следовательно, их число редко превышает два-три десятка. Иногда применяют генераторы тестовых данных – специальные программы, формирующие данные в соответствии со спецификациями задаваемыми программистами. Данные могут группироваться в записи, имеющие установленные форматы. Тестовые данные могут также систематически или случайно выбираться из другого заданного набора данных для уменьшения их общего количества, над которыми выполняется тест.

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

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

- распечатка текущего состояния

- точка останова

- трассировка

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

Метод «точки останова» обычно применяют при разного рода зацикливаниях. В текст программы включают функции останова программы, например можно вывести на экран сообщение «Достигнута точка n» и вызвать функцию getch(). Этот метод имеет смысл объединять с методом деления пополам. Точка останова ставится в средине программы, если программа выполнилась до этой точки, что точка останова ставиться в средине второй половины программы, если нет, то в средине первой и т.д. Таким образом область поиска ошибки постепенно сужается.

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

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

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

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

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

Первый способ основан на следующем правиле. Сложение и вычитание выполняются быстрее, чем умножение и деление.Целочисленная арифметика быстрее арифметики вещественных чисел. Таким образом, X+X лучше, чем 2*X, а (2i+j)*0,5 хуже, чем (i+i+j)*0,5. Умножение выолняется быстрее чем деление, например sum*=0,5 лучше чем sum/=2;При выполнении операций над целыми числами следует помнить, что благодаря применению двоичной системы счисления умножение числа, кратные двум, можно заменить соответствующим количеством сдвигов влево. Поэтому 10*А выполняется дольше, чем (A<<3)+(A<<1). (Действительно 10*A=8*A+2*A).

Второй способ заключается вудалении избыточных вычислений.

Пример, вычисление квадратных корней уровнения

root1=(-b+sqrt(b*b-4*a*c))/(2*a);

root2=(-b+sqrt(b*b-4*a*c))/(2*a);

Лучшим решением является следующее

denomA=a+a;

denomC=c+c;

diskrim=sqtr(b*b-denomA*demonС);

root1=(-b+diskrim)/demomA;

root2=(-b-diskrim)/demomA;

Легко увидеть, что в первом случае потребовалось выполнить четыре операции сложения/вычитания, восемь операций умножения, две операции деления, два вызова функции sqrt, во втором — пять операций сложения/вычитания, два умножения, два деления и одно обращение к функции sqrt.

Третий способ проверки эффективности реализации основан на способности некоторых компиляторов строить коды для вычисления логических выражений так, что вычисления прекращаются, если результат становится очевидным. Например, в выражении A||B||C, если A имеет значение «истина», то переменные В и С уже не проверяются. Таким образом, можно сэкономить время, разместив переменные A,B,С так, чтобы первой стояла переменная, которая вероятнее всего будет истинной, а последней та, которая реже всего принимает истинное значение.

Однако следует быть осторожным в следующем примере: ROOL(A)||B||C. ROOL(A) может и чаще принимает значение «истина», но представляет собой вызов функции, возможно выполняющей сложные и длительные вычисления. Тогда может оказаться, что запись В||С||ROOL(A) является более эффективной.

Четвертый прием - исключение циклов.

Пример. Рассмотрим следующий пример: сформировать одномерный массив, каждый элемент которого должен быть равен сумме элементов строки двумерного массива.

for (i=0;i<1000;i++) a[i]=0;

for (i=0;i<1000;i++)

for (j=0;j<10;j++) a[i]+=c[i][j];

можно переписать так

for (i=0;i<1000;i++)

{

b=c[i][0];

for (j=1;j<10;j++)

b+=c[i][j];

a[i]=b;

}

В данном пример выигрыш достигается, во-первых, за счет уменьшения количества циклов (два, а не три), а во-вторых, за счет того, что с введением временной переменной b уменьшено количество операций вычисления адресов элементов массива.

Пятый прием – развертывание циклов.

Пример. Следующий фрагмент

for(i=0; i<1000; i++)

for(j=0; j<3; j++)

a[i]+=c[i][j];

можно переписать так:

for(i=0; i<1000; i++)

a[i]+=c[i][0]+c[i][1]+c[i][2];

Выигрыш в скорости вычислений вполне очевиден.

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

Например,

for (int i=0; i<1000; i++)

{

if (P==S) y=1;

else y=S+25;

x[i]=(y+i)/2;

}

лучше переписать

if (P==S) y=1;

else y=S+25;

for (int i=0; i<1000; i++)

x[i]=(y+i)/2;

В данном случае используется так называемая чистка цикла вверх.Однако в следующем примере таким образом нельзя разгрузить цикл:

for (int i=0; i<10; i++)

{

x*=i;

if (z>0) c=chr(x);

else a=x+2;

}

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

for (int i=0; i<10; i++) x*=i;

if (z>0) c=chr(x);

else a=x+2;

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

- удаление идентичных операторов;

- удаление несущественных операторов, то есть операторов не влияющих на результат программы;

- удаление бесполезных операторов, вычисляющих вспомогательные переменные, используемые только для постановки в другие выражения;

- удаление функций, к которым нет обращений;

- удаление объявленных, но неиспользуемых переменных, операций и т.д.

Также существует ряд способов экономного расходования памяти:

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

- изменение времени жизни переменной

- перемещение оператора объявление переменной ближе к фрагменту, где переменная используется

- экономия стека: при передаче массива в качестве параметра подпрограммы, следует использовать указатель на массив.

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

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

Внедрение и сопровождение

Внедрение – это процесс запуска программы в промышленную эксплуатацию. Этот этап характерен для программ, разрабатываемых на заказ. При внедрении разработчик устанавливает продукт на компьютеры заказчика (инсталлирует его) и проверяет весь его рабочий цикл. Это этап эксплуатации системы. Если программа работает устойчиво, начинается этап обучения пользователей. (В договоре необходимо заранее указать объем учебных часов, которые разработчики должны посвятить обучению заказчика.)

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

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

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

Наши рекомендации