Проблемы, связанные с оптимизирующими компиляторами
Оптимизирующие компиляторы могут переопределить порядок следования инструкций в программе и сохранить конкретные переменные в регистрах центрального процессора, а не в оперативной памяти. Обычно такая оптимизация зависит от потока управления ходом выполнения программы, отражающего лексическую структуру программы. Поскольку операции перехода, выполняемые с помощью вызовов функций setjmp() и longjmp(), создаются и происходят в ходе выполнения программы, оптимизатор компилятора не может взять их в расчет в процессе своей работы. Более того, семантика некоторых реализаций двоичных интерфейсов приложений (ABI) требует, чтобы функция longjmp() восстанавливала копии регистров центрального процессора, сохраненные ранее при вызове функции setjmp().
Это говорит о том, что в результате вызова longjmp() в оптимизированных переменных могут оказаться неверные значения. Убедиться в этом можно, изучив поведение программы, представленной в листинге 6.6.
Листинг 6.6. Демонстрация взаимного влияния оптимизации при компиляции и функции longjmp()
proc/setjmp_vars.c
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
static jmp_buf env;
static void
doJump(int nvar, int rvar, int vvar)
{
printf("Inside doJump(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar);
longjmp(env, 1);
}
int
main(int argc, char *argv[])
{
int nvar;
register int rvar; /* По возможности выделяется в регистре */
volatile int vvar; /* Смотрите текст */
nvar = 111;
rvar = 222;
vvar = 333;
if (setjmp(env) == 0) { /* Код, выполняемый после setjmp() */
nvar = 777;
rvar = 888;
vvar = 999;
doJump(nvar, rvar, vvar);
} else { /* Код, выполняемый после longjmp() */
printf("After longjmp(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar);
}
exit(EXIT_SUCCESS);
}
proc/setjmp_vars.c
При компиляции без оптимизации программы, представленной в листинге 6.6, мы увидим на выходе вполне ожидаемую информацию:
$ cc — o setjmp_vars setjmp_vars.c
$ ./setjmp_vars
Inside doJump(): nvar=777 rvar=888 vvar=999
After longjmp(): nvar=777 rvar=888 vvar=999
Но при компиляции с оптимизацией будут получены такие неожиданные результаты:
$ cc — O — o setjmp_vars setjmp_vars.c
$ ./setjmp_vars
Inside doJump(): nvar=777 rvar=888 vvar=999
After longjmp(): nvar=111 rvar=222 vvar=999
Здесь видно, что после вызова longjmp() переменные nvar и rvar были переопределены, получив значения, имевшиеся у них ко времени вызова функции setjmp(). Это произошло потому, что вследствие вызова longjmp() реорганизация оптимизатором кода привела к путанице. Эта проблема может коснуться любых локальных переменных, являющихся кандидатами на оптимизацию. Как правило, она касается переменных-указателей и переменных любого простого типа: char, int, float и long.
Подобной реорганизации кода можно избежать, объявив переменные изменяемыми — volatile, что даст указание оптимизатору не оптимизировать их. В предыдущем выводе информации из программы было показано, что переменная vvar, объявленная volatile, была правильно обработана даже при компиляции с оптимизацией.
Поскольку различные компиляторы используют различные приемы оптимизации, в портируемых программах в тех функциях, которые вызывают setjmp(), ключевое слово volatile должно указываться со всеми локальными переменными вышеупомянутых типов.
Если компилятору GNU C задать ключ — Wextra (extra warnings — «дополнительные предупреждения»), то в отношении программы setjmp_vars.c он выдаст следующие полезные предупреждения:
$ cc — Wall — Wextra — O — o setjmp_vars setjmp_vars.c
setjmp_vars.c: In function 'main':
setjmp_vars.c:17: warning: variable 'nvar' might be clobbered
by 'longjmp' or 'vfork'
(Переменная nvar может быть «затерта» функцией longjmp или vfork.)
setjmp_vars.c:18: warning: variable 'rvar' might be clobbered
by 'longjmp' or 'vfork'
(Переменная rvar может быть «затерта» функцией longjmp или vfork.)
Поучительно будет взглянуть на ассемблерный выход, создаваемый при компиляции программы setjmp_vars.c как с оптимизацией, так и без нее. Команда cc — S создает файл с расширением. s, где содержится сгенерированный для программы ассемблерный код.
Использовать ли функции setjmp() и longjmp()
Выше говорилось, что инструкции переходов goto могут создавать трудности при чтении программы. В свою очередь, нелокальные переходы могут на порядок затруднить чтение, поскольку способны передавать управление между двумя любыми функциями программы. Таким образом, использование функций setjmp() и longjmp() должно стать редким исключением. Лучше потратить дополнительные усилия в проектировании и написании кода, чтобы получить программу, в которой удастся обойтись без этих функций, и в результате она станет легче читаемой и, возможно, более портируемой. Мы еще будем рассматривать варианты этих функций (sigsetjmp() и siglongjmp(), описание которых дается в подразделе 21.2.1) при изучении сигналов, поскольку их иногда полезно применять при написании обработчиков сигналов.