Одной из самый угнетающих вещей для каждого программиста является осознание того, что все ваше время тратится не на создание чего нибудь полезного, а на устранение проблем, которые мы же сами и создаем.
Этот процесс называется отладка. Каждый день, каждый программист предстает перед тем фактом, что когда он пишет код — он создает и ошибки в коде. И как только он понимает, что его программа не работает, он должен искать проблемы, которые сам же и создал.
Для решения таких проблем, компьютерная индустрия создала огромное количество инструментов, которые помогают вам убедится, что программа работает правильно. Программисты, для поиска ошибок, используют методы непрерывной интеграции, unit-тестирование, утверждения, отладчики, и т.д. Но ошибки все равно остаются, и должны быть устранены с помощью человеческого мышления.
Некоторые языки программирования, такие например как С, чрезвычайно восприимчивы к таким видам ошибок, которые появляются и исчезают случайным образом, и как только вы начинаете разбираться в причине их появления они сразу пропадают. Такие ошибки часто называют Гейзенбагами, потому что как только вы начинаете их искать, они исчезают.
Такие ошибки могут возникать в любом языке программирования, особенно при написании многозадачного кода, где малейшие задержки во времени могут вызвать состояние гонки. Но в C есть другая проблема — утечка памяти.
Однако, что бы не вызвало ошибку, ключевые шаги при поиске проблемы всегда следующие:
Недавно, в Hacker News появилась стаьтя — Если у вас гейзенбаг в С — значит проблемма в вашем оптимизаторе компилятора. Это очень неверное суждение.
Компилятор, который вы используете, вероятно, используют тысячи людей, в то время как вашу программу используете скорее всего только вы. Как вы думаете, что, компилятор или ваша программа работает наиболее стабильно?
В самом деле, признаком неопытности программиста является тот факт, что первое, что они делают при поиске ошибки — сваливают вину на кого-то другого. Очень заманчиво винить компьютер, операционную систему, библиотеку, которую вы используете. Однако настоящий программист тот, кто может контролировать своё «Я» и осознать что ошибка вероятнее всего его.
Конечно, ошибки бывают и в коде других программистов. Нет сомнений, что библиотека может не работать, операционная система может делать непонятные вещи, а компилятор генерировать странный код. Но чаще всего — это ваша ошибка, и это применимо даже в том случае, когда ошибка выглядит слишком странно.
В процессе отладки вы, зачастую, бьетесь головой об собственный код и повторяете вновь и вновь самые невозможные вещи, которые просто не могут произойти с вашим кодом. Однако в один прекрасный момент невозможная становится возможной и тогда вы находите ошибку.
В статье выше есть один определенно не завершенный пример:
«Отключите оптимизатор и проверьте программу снова. Если она работает — значит проблема в оптимизаторе. Поиграйтесь с уровнями оптимизации, поднимая уровень до тех пор, пока ошибка не начнет воспроизводится.»
Все что вы знаете когда меняете уровни оптимизации — это то, что уровни меняются независимо от того, появляется ошибка или нет. Это не говорит вам о том, что оптимизатор работает неверно. Вы не нашли искомую причину ошибки.
Так как оптимизаторы производят манипуляции с кодом для ускорения их работы, вполне вероятно, что в зависимости от уровня оптимизации гейзенбаги могут как появляться так и исчезать. Это ещё не значит что оптимизатор работает неправильно. Это все ещё, вероятнее всего, ваша ошибка.
Вот конкретный пример программы на C которая содержит ошибку, появляющуюся при изменении уровня оптимизации компилятора, и показывает странное поведение программы.
Скомпилируйте эту программу с помощью gcc под Mac OS X с помощью следующего Makefile (я сохранил код в файле odd.c).
И вот пример скрипта, который запускает программу 20 раз и выводит результат:
Если вы запустите этот скрипт, вы будете ожидать строку нулей, поскольку rc[0] никогда не получает значений отличных от нуля. Однако вот пример работы программы:
Если вы опытный программист C, вы увидите как я сделал чтобы появлялась единица, и почему она появляется в разных местах. Но давайте теперь попробует отладить программу с помощью printf:
Теперь, когда вы запустите программу, ошибка исчезнет.
Выглядит странно, поэтому вы перемещаете printf в другое место:
и получаете тот же странный результат с исчезновением ошибки. И то же самое произойдет если вы отключите оптимизатор и даже без printf ошибка не будет появляться:
Это все происходит, потому что ф-я a() выделяет память для 16-ти Integer элементов. И тут же записывает после конца массива либо 1 либо 0 в зависимости от того, делится ли PID процесса на 19 или нет. В конечном итогде она записывается в rc[0] из-за расположения в стеке.
Добавление printf или изменение уровня оптимизации меняет расположение кода и исключает неверное обращение к rc[0]. Но, будьте осторожны, ошибка не ушла. Единица просто записалась в другую ячейку памяти.
Т.к. C очень восприимчив к этому типу ошибок, важно использовать хорошие инструменты для проверки таких проблем. Например статический анализатор кода splint и анализатор памяти valgrind помогают устранить массу мерзких ошибок. И вы должны разрабатывать свои приложения с максимальным уровнем ошибок и устранять их все.
Только если вы сделаете все что нужно, вы можете начать подозревать чужой код. Но даже если вы начали это делать — проверьте ещё раз все шаги, чтобы установить истинную причину ошибки. К сожалению, в большинстве случаев, большая часть ошибок — ваша.
Этот процесс называется отладка. Каждый день, каждый программист предстает перед тем фактом, что когда он пишет код — он создает и ошибки в коде. И как только он понимает, что его программа не работает, он должен искать проблемы, которые сам же и создал.
Для решения таких проблем, компьютерная индустрия создала огромное количество инструментов, которые помогают вам убедится, что программа работает правильно. Программисты, для поиска ошибок, используют методы непрерывной интеграции, unit-тестирование, утверждения, отладчики, и т.д. Но ошибки все равно остаются, и должны быть устранены с помощью человеческого мышления.
Некоторые языки программирования, такие например как С, чрезвычайно восприимчивы к таким видам ошибок, которые появляются и исчезают случайным образом, и как только вы начинаете разбираться в причине их появления они сразу пропадают. Такие ошибки часто называют Гейзенбагами, потому что как только вы начинаете их искать, они исчезают.
Такие ошибки могут возникать в любом языке программирования, особенно при написании многозадачного кода, где малейшие задержки во времени могут вызвать состояние гонки. Но в C есть другая проблема — утечка памяти.
Однако, что бы не вызвало ошибку, ключевые шаги при поиске проблемы всегда следующие:
- Найти наименьшую закономерность, при которой ошибка полностью воспроизводится. С гейзенбагами это может оказаться сложным, но даже небольшой процент испытаний, при которых ошибка воспроизводится является значимым.
- Автоматизировать процесс испытаний. Намного лучше, когда можно запускать тест снова и снова. Можно даже сделать его частью программы, когда ошибка будет устранена — это не допустит появления ошибки вновь.
- Искать причину, пока не будет найдена её основа. До тех пор, пока вы не найдете истинную причину возникновения ошибки, вы не можете с уверенностью говорить, что вы её исправили. С гейзенбагами можно очень просто сбиться с толку, полагая, что вы исправили ошибку, после того как она вдруг исчезнет после того что вы сделаете в процессе поиска.
- Исправить причину и проверить с помощью шага 2.
Недавно, в Hacker News появилась стаьтя — Если у вас гейзенбаг в С — значит проблемма в вашем оптимизаторе компилятора. Это очень неверное суждение.
Компилятор, который вы используете, вероятно, используют тысячи людей, в то время как вашу программу используете скорее всего только вы. Как вы думаете, что, компилятор или ваша программа работает наиболее стабильно?
В самом деле, признаком неопытности программиста является тот факт, что первое, что они делают при поиске ошибки — сваливают вину на кого-то другого. Очень заманчиво винить компьютер, операционную систему, библиотеку, которую вы используете. Однако настоящий программист тот, кто может контролировать своё «Я» и осознать что ошибка вероятнее всего его.
Конечно, ошибки бывают и в коде других программистов. Нет сомнений, что библиотека может не работать, операционная система может делать непонятные вещи, а компилятор генерировать странный код. Но чаще всего — это ваша ошибка, и это применимо даже в том случае, когда ошибка выглядит слишком странно.
В процессе отладки вы, зачастую, бьетесь головой об собственный код и повторяете вновь и вновь самые невозможные вещи, которые просто не могут произойти с вашим кодом. Однако в один прекрасный момент невозможная становится возможной и тогда вы находите ошибку.
В статье выше есть один определенно не завершенный пример:
«Отключите оптимизатор и проверьте программу снова. Если она работает — значит проблема в оптимизаторе. Поиграйтесь с уровнями оптимизации, поднимая уровень до тех пор, пока ошибка не начнет воспроизводится.»
Все что вы знаете когда меняете уровни оптимизации — это то, что уровни меняются независимо от того, появляется ошибка или нет. Это не говорит вам о том, что оптимизатор работает неверно. Вы не нашли искомую причину ошибки.
Так как оптимизаторы производят манипуляции с кодом для ускорения их работы, вполне вероятно, что в зависимости от уровня оптимизации гейзенбаги могут как появляться так и исчезать. Это ещё не значит что оптимизатор работает неправильно. Это все ещё, вероятнее всего, ваша ошибка.
Вот конкретный пример программы на C которая содержит ошибку, появляющуюся при изменении уровня оптимизации компилятора, и показывает странное поведение программы.
#include <stdlib.h>
int a()
{
int ar[16];
ar[20] = (getpid() % 19 == 0);
}
int main(int argc, char * argv[])
{
int rc[16];
rc[0] = 0;
a();
return rc[0];
}
Скомпилируйте эту программу с помощью gcc под Mac OS X с помощью следующего Makefile (я сохранил код в файле odd.c).
CC=gcc
CFLAGS=
odd: odd.o
И вот пример скрипта, который запускает программу 20 раз и выводит результат:
#!/bin/bash
for i in {0..20}
do
./odd ; echo -n "$? "
done
echo
Если вы запустите этот скрипт, вы будете ожидать строку нулей, поскольку rc[0] никогда не получает значений отличных от нуля. Однако вот пример работы программы:
$ ./test
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Если вы опытный программист C, вы увидите как я сделал чтобы появлялась единица, и почему она появляется в разных местах. Но давайте теперь попробует отладить программу с помощью printf:
[...]
rc[0] = 0;
printf( "[%d]", rc[0] );
a();
[...]
Теперь, когда вы запустите программу, ошибка исчезнет.
$ ./test
[0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0
[0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0 [0]0
Выглядит странно, поэтому вы перемещаете printf в другое место:
[...]
rc[0] = 0;
a();
printf( "[%d]", rc[0] );
[...]
и получаете тот же странный результат с исчезновением ошибки. И то же самое произойдет если вы отключите оптимизатор и даже без printf ошибка не будет появляться:
$ make CFLAGS=-O3
gcc -O3 -c -o odd.o odd.c
gcc odd.o -o odd
$ ./test
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Это все происходит, потому что ф-я a() выделяет память для 16-ти Integer элементов. И тут же записывает после конца массива либо 1 либо 0 в зависимости от того, делится ли PID процесса на 19 или нет. В конечном итогде она записывается в rc[0] из-за расположения в стеке.
Добавление printf или изменение уровня оптимизации меняет расположение кода и исключает неверное обращение к rc[0]. Но, будьте осторожны, ошибка не ушла. Единица просто записалась в другую ячейку памяти.
Т.к. C очень восприимчив к этому типу ошибок, важно использовать хорошие инструменты для проверки таких проблем. Например статический анализатор кода splint и анализатор памяти valgrind помогают устранить массу мерзких ошибок. И вы должны разрабатывать свои приложения с максимальным уровнем ошибок и устранять их все.
Только если вы сделаете все что нужно, вы можете начать подозревать чужой код. Но даже если вы начали это делать — проверьте ещё раз все шаги, чтобы установить истинную причину ошибки. К сожалению, в большинстве случаев, большая часть ошибок — ваша.