В компьютерной науке цепочка определения использования (или цепочка UD ) представляет собой структуру данных , которая состоит из использования U , переменной и всех определений D этой переменной, которые могут достичь этого использования без каких-либо других промежуточных определений. [1] [2] Цепочка UD обычно означает присвоение некоторого значения переменной.
Аналогом цепочки UD является цепочка определения-использования (или цепочка DU ), которая состоит из определения D переменной и всех использований U, достижимых из этого определения без каких-либо других промежуточных определений. [3]
Цепочки UD и DU создаются с использованием формы статического анализа кода , известной как анализ потока данных . Знание цепочек use-def и def-use для программы или подпрограммы является предпосылкой для многих оптимизаций компилятора , включая распространение констант и устранение общих подвыражений .
Создание цепочек «использование-определение» или «определение-использование» является шагом в анализе жизнеспособности , позволяющим идентифицировать и отслеживать логические представления всех переменных в коде.
Рассмотрим следующий фрагмент кода:
int x = 0 ; /* A */ x = x + y ; /* B */ /* 1, некоторые примеры использования x */ x = 35 ; /* C */ /* 2, еще некоторые примеры использования x */
Обратите внимание, что x
присваивается значение в трех точках (отмеченных как A, B и C). Однако в точке, отмеченной как "1", цепочка use-def for x
должна указывать, что ее текущее значение должно быть получено из строки B (а ее значение в строке B должно быть получено из строки A). Напротив, в точке, отмеченной как "2", цепочка use-def for x
указывает, что ее текущее значение должно быть получено из строки C. Поскольку значение x
в блоке 2 не зависит ни от каких определений в блоке 1 или более ранних, x
то там может быть и другая переменная; по сути, это другая переменная — назовем ее x2
.
int x = 0 ; /* A */ x = x + y ; /* B */ /* 1, некоторые использования x */ int x2 = 35 ; /* C */ /* 2, некоторые использования x2 */
Процесс разделения x
на две отдельные переменные называется живым разделением диапазона. См. также статическую форму одиночного назначения .
Список утверждений определяет строгий порядок среди утверждений.
Для переменной, такой как v , ее объявление идентифицируется как V (курсивная заглавная буква), и для краткости ее объявление идентифицируется как . В общем случае объявление переменной может находиться во внешней области видимости (например, глобальная переменная ).
Когда переменная v находится в левой части оператора присваивания, например, , то является определением v . Каждая переменная ( v ) имеет по крайней мере одно определение посредством своего объявления ( V ) (или инициализации).
Если переменная v находится в правой части оператора , то существует оператор с i < j и , который является определением v и имеет применение в (или, короче говоря, когда переменная v находится в правой части оператора , то v имеет применение в операторе ).
Рассмотрим последовательное выполнение списка операторов, , и то, что теперь можно наблюдать как вычисление в операторе, j :
Этот пример основан на алгоритме Java для нахождения НОД . (Не важно понимать, что делает эта функция.)
/*** @param(a, b) Значения, используемые для вычисления делителя.* @return Наибольший общий делитель a и b.*/int gcd ( int a , int b ) { int c = a ; целое число d = b ; если ( с == 0 ) вернуть д ; пока ( д != 0 ) { если ( с > г ) с = с - d ; еще д = д - с ; } вернуть c ; }
Чтобы узнать все def-use-chains для переменной d, выполните следующие действия:
d=b
" (л.7)return d
"[d, d=b, return d]
Повторите эти шаги в следующем стиле: объедините каждый доступ на запись с каждым доступом на чтение (но НЕ наоборот).
Результат должен быть:
[ d , d = b , вернуть d ] [ d , d = b , в то время как ( d != 0 )] [ d , d = b , если ( c > d )] [ д , д = б , с = с - д ] [ д , д = б , д = д - в ] [ d , d = d - c , пока ( d != 0 )] [ d , d = d - c , если ( c > d )] [ д , д = д - с , с = с - д ] [ д , д = д - с , д = д - с ]
Вам следует быть осторожным, если переменная со временем изменится.
Например: от строки 7 до строки 13 в исходном коде d не переопределяется/не изменяется. В строке 14 d может быть переопределен. Вот почему вам нужно перекомбинировать этот доступ на запись к d со всеми возможными доступами на чтение, которые могут быть достигнуты. В этом случае важен только код после строки 10. Например, строка 7 не может быть достигнута снова. Для вашего понимания вы можете представить себе 2 разные переменные d :
[ d1 , d1 = b , вернуть d1 ] [ d1 , d1 = b , в то время как ( d1 != 0 )] [ d1 , d1 = b , если ( c > d1 )] [ d1 , d1 = b , c = c - d1 ] [ d1 , d1 = b , d1 = d1 - c ] [ d2 , d2 = d2 - c , пока ( d2 != 0 )] [ d2 , d2 = d2 - c , если ( c > d2 )] [ d2 , d2 = d2 - с , с = с - d2 ] [ d2 , d2 = d2 - с , d2 = d2 - с ]
В результате вы могли бы получить что-то вроде этого. Переменная d1 будет заменена на b
/*** @param(a, b) Значения, используемые для вычисления делителя.* @return Наибольший общий делитель a и b.**/int gcd ( int a , int b ) { int c = a ; целочисленный d ; если ( с == 0 ) вернуть б ; если ( б != 0 ) { если ( с > б ) { с = с - б ; г = б ; } еще г = б - в ; пока ( д != 0 ) { если ( с > г ) с = с - d ; еще д = д - с ; } } вернуть c ; }
С помощью этого алгоритма достигаются две вещи: