Машинно-зависимые методы оптимизации

 

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

Архитектура вычислительной системы – представление аппаратной и програм­мной составляющих частей системы и взаимосвязи между ними с точки зрения системы как единого целого. Понятие “архитектура” включает в себя особенно­сти и аппаратных, и программных средств целевой вычислительной системы. При выполнении машинно-зависимой оптимизации компилятор может прини­мать во внимание те или иные ее составляющие. То, какие конкретно особенно­сти архитектуры будут приняты во внимание, зависит от реализации компилято­ра и определяется его разработчиками.

Распределение регистров процессора.Процессоры, на базе которых строятся современные вычислительные системы, имеют, как правило, несколько программно-доступных регистров. Часть из них может быть предназначена для выполнения каких-либо определенных целей (на­пример, регистр – указатель стека или регистр – счетчик команд), другие могут быть использованы практически произвольным образом при выполнении раз­личных операций (так называемые “регистры общего назначения”).

Использование регистров общего назначения для хранения значений операндов и результатов вычислений позволяет добиться увеличения быстродействия про­граммы, так как действия над регистрами процессора всегда выполняются быст­рее, чем над ячейками памяти. Кроме того, в ряде процессоров не все операции могут быть выполнены над ячейками памяти, а потому часто требуется предва­рительная загрузка операнда в регистр. Результат выполнения операции чаще всего тоже оказывается в регистре, и если необходимо, его надо выгрузить (запи­сать) в ячейку памяти.

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

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

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

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

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

Например, операцию A+B+C+D+E+F на процессоре с одним потоком обработки дан­ных лучше выполнять в порядке ((((A+B)+C)+D)+E)+F. Тогда потребуется меньше ячеек для хранения промежуточных результатов, а скорость выполнения от по­рядка операций в данном случае не зависит.

Та же операция на процессоре с двумя потоками обработки данных в целях уве­личения скорости выполнения может быть обработана в порядке ((А+В)+С)+ +((D+E)+F). Тогда, по крайней мере, операции А+В и D+E, а также сложение с их ре­зультатами могут быть обработаны в параллельном режиме. Конкретный поря­док команд, а также распределение регистров для хранения промежуточных ре­зультатов будут зависеть от типа процессора.

На процессоре с тремя потоками обработки данных ту же операцию можно уже разбить на части в виде (A+B)+(C+D)+(E+F). Теперь уже три операции А+В, C+D и E+F могут быть выполнены параллельно. Правда, их результаты уже должны быть обработаны последовательно, но тут уже следует принять во внимание соседние операции для нахождения наиболее оптимального варианта.