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