В программировании thunk — это подпрограмма, используемая для внедрения вычисления в другую подпрограмму. Thunk в основном используются для задержки вычисления до тех пор, пока не понадобится его результат, или для вставки операций в начало или конец другой подпрограммы. Они имеют много других применений в генерации кода компилятора и модульном программировании .
Термин возник как причудливая неправильная форма глагола think . Он относится к первоначальному использованию thunks в компиляторах ALGOL 60 , которые требовали специального анализа (мысли) для определения типа генерируемой процедуры. [1] [2]
В первые годы исследований компиляторов проводились обширные эксперименты с различными стратегиями оценки . Ключевым вопросом было, как скомпилировать вызов подпрограммы, если аргументы могут быть произвольными математическими выражениями, а не константами. Один подход, известный как « вызов по значению », вычисляет все аргументы перед вызовом, а затем передает полученные значения в подпрограмму. В конкурирующем подходе « вызов по имени » подпрограмма получает неоцененное выражение аргумента и должна его оценить.
Простая реализация «вызова по имени» может заменить код выражения аргумента для каждого появления соответствующего параметра в подпрограмме, но это может создать несколько версий подпрограммы и несколько копий кода выражения. В качестве улучшения компилятор может сгенерировать вспомогательную подпрограмму, называемую thunk , которая вычисляет значение аргумента. Адрес и окружение [a] этой вспомогательной подпрограммы затем передаются исходной подпрограмме вместо исходного аргумента, где она может быть вызвана столько раз, сколько необходимо. Питер Ингерман впервые описал thunks в отношении языка программирования ALGOL 60, который поддерживает оценку вызова по имени. [4]
Хотя индустрия программного обеспечения в значительной степени стандартизировала оценку вызовов по значению и вызовов по ссылке , [5] активное изучение вызова по имени продолжалось в сообществе функционального программирования . Это исследование привело к появлению ряда языков программирования с ленивой оценкой , в которых некоторый вариант вызова по имени является стандартной стратегией оценки. Компиляторы для этих языков, такие как Glasgow Haskell Compiler , в значительной степени полагались на thunks, с дополнительной функцией, заключающейся в том, что thunks сохраняют свой начальный результат, чтобы они могли избежать его повторного вычисления; [6] это известно как мемоизация или вызов по необходимости .
Функциональные языки программирования также позволили программистам явно генерировать thunks. Это делается в исходном коде путем оборачивания выражения аргумента в анонимную функцию , которая не имеет собственных параметров. Это предотвращает вычисление выражения до тех пор, пока принимающая функция не вызовет анонимную функцию, тем самым достигая того же эффекта, что и вызов по имени. [7] Внедрение анонимных функций в другие языки программирования сделало эту возможность широко доступной.
Ниже приведена простая демонстрация на JavaScript (ES6):
// 'hypot' — это бинарная функция const hypot = ( x , y ) => Math . sqrt ( x * x + y * y ); // 'thunk' — это функция, которая не принимает аргументов и при вызове выполняет потенциально затратную операцию (в данном примере — вычисление квадратного корня) и/или вызывает некоторый побочный эффект const thunk = () => hypot ( 3 , 4 ); // затем thunk можно передавать без оценки... doSomethingWithThunk ( thunk );// ...или вычисленный thunk (); // === 5
Thunks полезны в объектно-ориентированных программных платформах, которые позволяют классу наследовать несколько интерфейсов , что приводит к ситуациям, когда один и тот же метод может быть вызван через любой из нескольких интерфейсов. Следующий код иллюстрирует такую ситуацию в C++ .
класс A { public : virtual int Access () const { return value_ ; } частный : int value_ ; }; класс B { public : virtual int Access () const { return value_ ; } частный : int value_ ; }; класс C : public A , public B { public : int Access () const override { return better_value_ ; } частный : int better_value_ ; }; int use ( B * b ) { return b -> Access (); } int main () { // ... B some_b ; использование ( & some_b ); C some_c ; использование ( & some_c ); }
В этом примере код, сгенерированный для каждого из классов A, B и C, будет включать таблицу диспетчеризации , которая может использоваться для вызова Access
объекта этого типа через ссылку, имеющую тот же тип. Класс C будет иметь дополнительную таблицу диспетчеризации, используемую для вызова объекта Access
типа C через ссылку типа B. Выражение b->Access()
будет использовать собственную таблицу диспетчеризации B или дополнительную таблицу C, в зависимости от типа объекта, на который ссылается b. Если он ссылается на объект типа C, компилятор должен гарантировать, что Access
реализация C получит адрес экземпляра для всего объекта C, а не унаследованной части B этого объекта. [8]
В качестве прямого подхода к этой проблеме корректировки указателя компилятор может включить целочисленное смещение в каждую запись таблицы диспетчеризации. Это смещение представляет собой разницу между адресом ссылки и адресом, требуемым реализацией метода. Код, сгенерированный для каждого вызова через эти таблицы диспетчеризации, должен затем извлечь смещение и использовать его для корректировки адреса экземпляра перед вызовом метода.
Описанное выше решение имеет проблемы, схожие с наивной реализацией вызова по имени, описанной ранее: компилятор генерирует несколько копий кода для вычисления аргумента (адреса экземпляра), а также увеличивает размеры таблицы диспетчеризации для хранения смещений. В качестве альтернативы компилятор может сгенерировать thunk-регулятор вместе с реализацией C, Access
которая корректирует адрес экземпляра на требуемую величину, а затем вызывает метод. Thunk может появиться в таблице диспетчеризации C для B, тем самым устраняя необходимость для вызывающих программ самостоятельно корректировать адрес. [9]
Подпрограммы для вычислений, таких как интегрирование, должны вычислять выражение в нескольких точках. Вызов по имени использовался для этой цели в языках, которые не поддерживали замыкания или параметры процедур .
Thunk широко использовались для обеспечения взаимодействия между программными модулями, чьи процедуры не могут вызывать друг друга напрямую. Это может произойти из-за того, что процедуры имеют разные соглашения о вызовах , работают в разных режимах ЦП или адресных пространствах или, по крайней мере, одна из них работает в виртуальной машине . Компилятор (или другой инструмент) может решить эту проблему, создав thunk, который автоматизирует дополнительные шаги, необходимые для вызова целевой процедуры, будь то преобразование аргументов, копирование их в другое место или переключение режима ЦП. Успешный thunk минимизирует дополнительную работу, которую должен выполнить вызывающий по сравнению с обычным вызовом.
Большая часть литературы по интероперабельности thunks относится к различным платформам Wintel , включая MS-DOS , OS/2 , [10] Windows [11] [12] [13] [14] и .NET , а также к переходу от 16-битной к 32-битной адресации памяти. Поскольку клиенты переходили с одной платформы на другую, thunks стали необходимы для поддержки устаревшего программного обеспечения, написанного для старых платформ.
Переход от 32-битного к 64-битному коду на x86 также использует форму преобразования ( WoW64 ). Однако, поскольку адресное пространство x86-64 больше, чем доступное для 32-битного кода, старый механизм «generic thunk» не мог использоваться для вызова 64-битного кода из 32-битного кода. [15] Единственный случай, когда 32-битный код вызывает 64-битный код, — это преобразование WoW64 API Windows в 32-битный.
В системах, в которых отсутствует автоматическое виртуальное аппаратное обеспечение памяти , thunks могут реализовывать ограниченную форму виртуальной памяти, известную как overlays . С помощью overlays разработчик делит код программы на сегменты, которые могут быть загружены и выгружены независимо, и определяет точки входа в каждый сегмент. Сегмент, который вызывает другой сегмент, должен делать это косвенно через таблицу ветвлений . Когда сегмент находится в памяти, записи его таблицы ветвлений переходят в сегмент. Когда сегмент выгружается, его записи заменяются «reload thunks», которые могут перезагружать его по требованию. [16]
Аналогично, системы, которые динамически связывают модули программы вместе во время выполнения, могут использовать thunks для соединения модулей. Каждый модуль может вызывать другие через таблицу thunks, которую компоновщик заполняет при загрузке модуля. Таким образом, модули могут взаимодействовать без предварительного знания того, где они находятся в памяти. [17]
{{cite book}}
: |work=
проигнорировано ( помощь )