Анализ потока данных — это метод сбора информации о возможном наборе значений, вычисляемых в различных точках компьютерной программы . Граф потока управления программы (CFG) используется для определения тех частей программы, на которые может распространяться определенное значение, назначенное переменной. Собранная информация часто используется компиляторами при оптимизации программы . Каноническим примером анализа потока данных является достижение определений .
Простой способ выполнить анализ потока данных программ — это составить уравнения потока данных для каждого узла графа потока управления и решить их, многократно вычисляя выход из входа локально в каждом узле, пока вся система не стабилизируется, т. е. не достигнет фиксированной точки .Этот общий подход, также известный как метод Килдалла , был разработан Гэри Килдаллом во время преподавания в Военно-морской аспирантуре . [1] [2] [3] [4]
Анализ потока данных — это процесс сбора информации о том, как переменные определяются и используются в программе. Он пытается получить определенную информацию в каждой точке процедуры. Обычно достаточно получить эту информацию на границах базовых блоков , так как из этого легко вычислить информацию в точках базового блока. В анализе прямого потока выходное состояние блока является функцией входного состояния блока. Эта функция представляет собой композицию эффектов операторов в блоке. Входное состояние блока является функцией выходных состояний его предшественников. Это дает набор уравнений потока данных:
Для каждого блока b:
В этом, есть передаточная функция блока . Она работает над входным состоянием , давая выходное состояние . Операция соединения объединяет выходные состояния предшественников , давая входное состояние .
После решения этого набора уравнений состояния входа и/или выхода блоков могут быть использованы для получения свойств программы на границах блоков. Передаточная функция каждого оператора в отдельности может быть применена для получения информации в точке внутри базового блока.
Каждый конкретный тип анализа потока данных имеет свою собственную специфическую функцию передачи и операцию соединения. Некоторые проблемы потока данных требуют анализа обратного потока. Это следует тому же плану, за исключением того, что функция передачи применяется к выходному состоянию, давая входное состояние, а операция соединения работает с входными состояниями преемников, давая выходное состояние.
Точка входа (в прямом потоке) играет важную роль: поскольку у нее нет предшественников, ее состояние входа хорошо определено в начале анализа. Например, набор локальных переменных с известными значениями пуст. Если граф потока управления не содержит циклов (в процедуре не было явных или неявных циклов ), решение уравнений является простым. Затем граф потока управления может быть топологически отсортирован ; работая в порядке такой сортировки, состояния входа могут быть вычислены в начале каждого блока, поскольку все предшественники этого блока уже были обработаны, поэтому их состояния выхода доступны. Если граф потока управления содержит циклы, требуется более продвинутый алгоритм.
Наиболее распространенный способ решения уравнений потока данных — использование итеративного алгоритма. Он начинается с аппроксимации входного состояния каждого блока. Выходные состояния затем вычисляются путем применения функций переноса к входным состояниям. Из них входные состояния обновляются путем применения операций соединения. Последние два шага повторяются до тех пор, пока мы не достигнем так называемой фиксированной точки : ситуации, в которой входные состояния (и выходные состояния в результате) не изменяются.
Базовым алгоритмом решения уравнений потока данных является циклический итерационный алгоритм :
Чтобы быть пригодным для использования, итеративный подход должен фактически достичь фиксированной точки. Это может быть гарантировано путем наложения ограничений на комбинацию области значений состояний, функций переноса и операции соединения.
Область значений должна быть частичным порядком с конечной высотой (т. е. не должно быть бесконечных восходящих цепей < < ...). Комбинация функции переноса и операции соединения должна быть монотонной относительно этого частичного порядка. Монотонность гарантирует, что на каждой итерации значение либо останется прежним, либо будет расти, в то время как конечная высота гарантирует, что оно не может расти бесконечно. Таким образом, в конечном итоге мы достигнем ситуации, когда T(x) = x для всех x, что является фиксированной точкой.
Легко улучшить алгоритм выше, заметив, что in-state блока не изменится, если out-state его предшественников не изменятся. Поэтому мы вводим рабочий список : список блоков, которые все еще нужно обработать. Всякий раз, когда out-state блока изменяется, мы добавляем его последователей в рабочий список. На каждой итерации блок удаляется из рабочего списка. Его out-state вычисляется. Если out-state изменился, последователи блока добавляются в рабочий список. Для эффективности блок не должен находиться в рабочем списке более одного раза.
Алгоритм запускается путем помещения в рабочий список блоков, генерирующих информацию. Он завершается, когда рабочий список становится пустым.
Эффективность итеративного решения уравнений потока данных зависит от порядка посещения локальных узлов. [5] Кроме того, это зависит от того, используются ли уравнения потока данных для прямого или обратного анализа потока данных по CFG. Интуитивно понятно, что в задаче прямого потока будет быстрее, если все предшественники блока будут обработаны до самого блока, поскольку тогда итерация будет использовать последнюю информацию. При отсутствии циклов можно упорядочить блоки таким образом, что правильные выходные состояния будут вычисляться путем обработки каждого блока только один раз.
Далее обсуждаются несколько порядков итераций для решения уравнений потока данных (связанной с порядком итераций CFG концепцией является обход дерева ) .
Начальное значение in-states важно для получения правильных и точных результатов. Если результаты используются для оптимизации компилятора, они должны предоставлять консервативную информацию, т. е. при применении информации программа не должна менять семантику. Итерация алгоритма fixpoint будет принимать значения в направлении максимального элемента. Поэтому инициализация всех блоков максимальным элементом бесполезна. По крайней мере один блок начинается в состоянии со значением меньше максимального. Детали зависят от проблемы потока данных. Если минимальный элемент представляет полностью консервативную информацию, результаты можно безопасно использовать даже во время итерации потока данных. Если он представляет наиболее точную информацию, fixpoint должен быть достигнут до того, как результаты могут быть применены.
Ниже приведены примеры свойств компьютерных программ, которые можно вычислить с помощью анализа потока данных. Обратите внимание, что свойства, вычисленные с помощью анализа потока данных, обычно являются лишь приближениями реальных свойств. Это связано с тем, что анализ потока данных работает с синтаксической структурой CFG, не имитируя точный поток управления программы. Однако, чтобы быть полезным на практике, алгоритм анализа потока данных обычно разрабатывается для вычисления верхнего и соответственно нижнего приближения реальных свойств программы.
Анализ достижения определения вычисляет для каждой точки программы набор определений, которые потенциально могут достичь этой точки программы.
если б == 4 тогда а = 5; еще а = 3; эндиф если а < 4 то ...
Достижение определения переменной a
в строке 7 представляет собой набор назначений a = 5
в строке 2 и a = 3
в строке 4.
Анализ живых переменных вычисляет для каждой точки программы переменные, которые могут быть потенциально прочитаны впоследствии перед их следующим обновлением записи. Результат обычно используется устранением мертвого кода для удаления операторов, которые присваивают переменной, значение которой впоследствии не используется.
Входное состояние блока — это набор переменных, которые активны в начале блока. Изначально он содержит все переменные, активные (содержащиеся) в блоке, до применения функции переноса и вычисления фактических содержащихся значений. Функция переноса оператора применяется путем уничтожения переменных, записанных в этом блоке (удаления их из набора активных переменных). Выходное состояние блока — это набор переменных, которые активны в конце блока и вычисляются путем объединения входных состояний преемников блока.
Исходный код:
Обратный анализ:
Входящее состояние b3 содержит только b и d , так как c было записано. Выходное состояние b1 является объединением входящих состояний b2 и b3. Определение c в b2 можно удалить, так как c не является активным сразу после оператора.
Решение уравнений потока данных начинается с инициализации всех входящих и исходящих состояний в пустой набор. Рабочий список инициализируется путем вставки точки выхода (b3) в рабочий список (типично для обратного потока). Его вычисленное входящее состояние отличается от предыдущего, поэтому вставляются его предшественники b1 и b2, и процесс продолжается. Прогресс суммирован в таблице ниже.
Обратите внимание, что b1 был введен в список до b2, что заставило обрабатывать b1 дважды (b1 был повторно введен как предшественник b2). Вставка b2 перед b1 позволила бы выполнить завершение раньше.
Инициализация с пустым набором — это оптимистичная инициализация: все переменные изначально мертвы. Обратите внимание, что выходные состояния не могут уменьшаться от одной итерации к другой, хотя выходное состояние может быть меньше входного. Это видно из того факта, что после первой итерации выходное состояние может измениться только путем изменения входного состояния. Поскольку входное состояние изначально пустое множество, оно может только расти в дальнейших итерациях.
Некоторые современные компиляторы используют статическую форму с одним присваиванием в качестве метода анализа зависимостей переменных. [6]
В 2002 году Маркус Монен описал новый метод анализа потока данных, который не требует явного построения графа потока данных [7] , вместо этого полагаясь на абстрактную интерпретацию программы и сохранение рабочего набора счетчиков программ. При каждом условном переходе обе цели добавляются в рабочий набор. Каждый путь выполняется для максимально возможного количества инструкций (до конца программы или до тех пор, пока она не зациклится без изменений), а затем удаляется из набора и извлекается следующий счетчик программ.
Сочетание анализа потока управления и анализа потока данных показало себя полезным и взаимодополняющим при определении связанных областей исходного кода, реализующих функциональные возможности системы (например, функции , требования или варианты использования ). [8]
Существует множество специальных классов задач, связанных с потоками данных, которые имеют эффективные или общие решения.
Приведенные выше примеры — это задачи, в которых значение потока данных представляет собой набор, например, набор достигающих определений (использование бита для позиции определения в программе) или набор живых переменных. Эти наборы могут быть эффективно представлены как битовые векторы , в которых каждый бит представляет принадлежность к набору одного конкретного элемента. Используя это представление, функции соединения и передачи могут быть реализованы как побитовые логические операции. Операция соединения обычно представляет собой объединение или пересечение, реализуемое побитовыми логическими ИЛИ и логическими И. Функция передачи для каждого блока может быть разложена на так называемые наборы gen и kill .
Например, в анализе живых переменных операция join — это union. Kill set — это набор переменных, которые записываются в блок, тогда как gen set — это набор переменных, которые считываются без предварительной записи. Уравнения потока данных становятся
В логических операциях это читается как
out( b ) = 0 для s in succ( b ) вых( б ) = вых( б ) или вх( с )in( b ) = (out( b ) и не kill( b )) или gen( b )
Задачи потока данных, которые имеют наборы значений потока данных, которые могут быть представлены в виде битовых векторов, называются задачами битовых векторов , задачами уничтожения генов или локально разделимыми задачами . [9] Такие задачи имеют общие решения за полиномиальное время. [10]
В дополнение к проблемам достижения определений и живых переменных, упомянутым выше, следующие проблемы являются примерами проблем битовых векторов: [10]
Межпроцедурные, конечные, распределительные, подмножественные проблемы или проблемы IFDS представляют собой еще один класс проблем с универсальным решением за полиномиальное время. [9] [11] Решения этих проблем обеспечивают контекстно-зависимый и чувствительный к потоку анализ потоков данных.
Существует несколько реализаций анализа потоков данных на основе IFDS для популярных языков программирования, например, в фреймворках Soot [12] и WALA [13] для анализа Java.
Каждая проблема с битовыми векторами также является проблемой IFDS, но существует несколько существенных проблем IFDS, которые не являются проблемами с битовыми векторами, включая действительно живые переменные и, возможно, неинициализированные переменные.
Анализ потока данных обычно нечувствителен к пути, хотя можно определить уравнения потока данных, которые дают анализ, чувствительный к пути.
x>0
, то на пути провала анализ будет предполагать, что x<=0
и на цели ветви он будет предполагать, что действительно x>0
выполняется.[…]
Юбэнкс
: […]
Гэри
[…] был изобретателем, он был изобретательным, он делал вещи. Его докторская диссертация доказала, что глобальный анализ потока сходится. […] Это фундаментальная идея в компьютерной науке. […] Однажды я прошел […] летний курс у парня по имени Дхамдхере […] они говорили об оптимизации где-то неделю, а потом вывесили слайд и сказали: «Метод Килдалла», вот настоящая история. […] это то, о чем никто никогда не думает. […][2][3] (33 страницы)