C-후부 반복 최적화
8921 단어 Haskellghc마지막 귀속 최적화cmmtech
구상적 독자
개시하다
Haskell의 대표적인 처리 시스템인 GHC에서 원본 코드는 C-로 컴파일됩니다.C-가 아닌 C-를 사용하는 이유 중 하나로 처리 시스템의 절차를 더욱 자유롭게 제어할 수 있다.하나의 예로 C-에서 결과가 돌아가는 (명확한) 최적화는 쉽게 표현할 수 있다.
하나의 예만 제시하고 문법은 설명하지 않는다.또한 이것은 저자의 환경(x86-64, Linux)의 예로 다른 환경에서 조정이 필요할 수 있다.
잘못이 있다면 부드럽게 지적해 주세요.여기에 소개된 코드는 다음과 같이 시작할 수 있다.
C--무엇이
C-어떤 느낌인지 다음 논문에서 정리한다.
간단히 설명하면 다음과 같다.
고급 언어의 컴파일러를 제작할 때 각자의 하드웨어에 대해 각자의 기계어를 생성하는 것이 아니라 어느 하드웨어에 공통된 중간적인 표현을 생성하여 각자의 기계어로 전환하는 코드 효율이 더욱 좋다.C 언어는 공통된 중간 표현으로 사용된 적이 있습니다.C 언어의 컴파일러가 널리 보급되어 좋은 선택이다.그러나 C언어는 이런'중간적 표현'으로 설계된 것이 아니어서 다양한 문제가 발생한다.그런 문제를 해결할 수 있도록 표현을 요구하다.그래서 C 언어와 어셈블리의 중간 표현인 C-를 설계했다.
C-의 컴파일러는 독립된 프로젝트로 몇 개 개발되었지만, 지금은 유지보수되지 않았다.따라서 GHC는 유지 관리되는 C-처리 시스템으로서 사용하기로 결정되었습니다.
C 함수main에서 호출
C 함수main에서 호출하여 C-로부터의 반환 값을 보여 줍니다.다음 코드를 준비하십시오.
fun.cmm
foo()
{
return(123);
}
준비foo
이런 수속(모듈).이것은 반환값으로 되돌아온다123
.다음에 foo
의 C 언어 코드를 호출할 준비를 하세요.call_cmm.c
#include <stdio.h>
int
main(int argc, char *argv[])
{
long ret;
__asm__(
"addq $-16, %%rsp\n\t"
"moveq %%rbp, 8(%%rsp)\n\t"
"moveq %%rsp, %%rbp\n\t"
"moveq $print_ret, (%%rbp)\n\t"
"jmp foo\n"
"print_ret:\n\t"
"moveq 8(%%rsp), %%rbp\n\t"
"addq $16, %%rsp\n"
: "=b"(ret)
:
: );
printf("ret: %ld\n", ret);
return 0;
}
함수main
에서 C-의foo
를 호출합니다.C-에서 %rbp
는 스택 포인터로 사용되기 때문에 라벨%rbp
의 값이 print_ret
가 가리키는 주소를 대입한다.%rsp
,%rbp
는 이 전후에 값을 바꾸고 싶지 않기 때문에 다음과 같이 처리한다.%rsp
에서 16%rsp
의 값에 8을 더한 주소%rbp
의 값을 대입%rsp
의 값을 %rbp
%rbp
의 값을 print_ret
의 값에 대입한 주소(으)로 건너뛰기
foo
foo
에서 %rbp
로 되돌아오는 값에 표시된 주소 처리print_ret
%rsp
의 값과 8의 주소로 되돌아오는 값%rbp
%rsp
에 16아마도, 이렇게 하면 C-의 처리를 안전하게 호출할 수 있을 것이다.번역해 보세요.
% ghc fun.cmm -no-hs-main call_cmm.c -o fun
% ./fun
ret: 123
GHC는 기본적으로 C의 함수main
입니다.이번에는 자기main
를 사용하고 싶어서 선택해야 한다-no-hs-main
.마지막 컴백의 최적화란
다음과 같은 처리 요구가 있습니다.
factorial
n
와 s
두 개n
비0
보다 크면n - 1
과s * n
를 매개 변수factorial
로 호출n
가 0
인 경우 s
의 값이 처리는
n
하나하나 1
줄어들고, 매번 s
는 n배이다.되돌아오는 값n
의 곱셈이다.C 언어의'함수 호출'로 이 방법을 실행하면 매번 호출factorial
할 때마다 창고에'귀환 주소'를 누적하고 n
가0
되더라도 처리는 창고에 누적된 주소를 순서대로 귀환한다.하지만 그런 일을 할 필요는 없다.
n
가 0
가 되는 단계에서 단숨에 최초의 호출원으로 돌아가면 된다.그럼 창고 구역도 낭비하지 않을 거예요.이렇게 하면'스택으로 되돌아와 스택된 주소'의 처리와'스택된 주소를 순서대로 추적하는 처리'를 생략하는 것이 마지막 스택의 최적화(가능)입니다.
계산 단계 처리 (최적화되지 않은 버전)
끝부분 귀속 최적화 버전의 '계산 곱셈 처리' 는 다음과 같다.
fac_no_opt.cmm
factorial(bits64 n, bits64 s)
{
if (n > 0) {
(bits64 ret) = call factorial(n - 1, s * n);
return(ret);
} else {
return(s);
}
}
foo()
{
(bits64 ret) = call factorial(10, 1);
return(ret);
}
대충 알 것 같아서요.소스 C 언어의 소스 코드 사용call_cmm.c
을 호출합니다.% ghc fac_no_opt.cmm -no-hs-main call_cmm.c -o fac_no_opt
% ./fac_no_opt
ret: 3628800
계산 곱셈 처리 (최적화 버전)
그럼 끝부분의 귀속 버전을 최적화해 봅시다.C-에서 호출 처리 방법은
call
뿐만 아니라 jump
도 요점이다.아래와 같다.fac_opt.cmm
factorial(bits64 n, bits64 s)
{
if (n > 0) {
jump factorial(n - 1, s * n);
} else {
return(s);
}
}
foo()
{
(bits64 ret) = call factorial(10, 1);
return(ret);
}
n
가 0
보다 클 때의 처리는 call
가 아니라 jump
이다.주프로 비행할 때 처리해. 돌아오지 않을 거야.창고 주소가 쌓인 것을 되돌려주는 일도 없었다.그리고 처리는 제n
회factorial
회n + 1
회return
명령 제1차factorial
호foo
로 되돌아갔다.여분의 창고 소비와 여분의 도약을 생략했다.번역해 보세요.% ghc fac_opt.cmm -no-hs-main call_cmm.c -o fac_opt
% ./fac_opt
ret: 3628800
1에서 n의 합을 계산하는 처리에서 비교하다
만약에'계승'이라면 결과가 너무 크다
1
부터 n
까지의 총화라는 제재는 전후 처리를 비교적 최적화시켰다.최적화 없음
최적화가 없으면 코드 개발을 진행할 수 있다.
sum_no_opt.cmm
summation(bits64 n, bits64 s)
{
if (n > 0) {
(bits64 ret) = call summation(n - 1, s + n);
return(ret)
} else {
return(s);
}
}
foo()
{
(bits64 ret) = call summation(2000000, 0);
return(ret);
}
곱셈을 화산으로 바꾸다.아무튼 n
부터 2000000
(200만 원)로 바꾸자.해봐.% ghc sum_no_opt.cmm -no-hs-main call_cmm.c -o sum_no_opt
% ./sum_no_opt
zsh: segmentation fault ./sum_no_opt
창고 구역을 다 썼나 봐요.부문별로 분류되다.최적화 후
마지막으로 귀속이 최적화 코드로 바뀌었다.
sum_opt.cmm
summation(bits64 n, bits64 s)
{
if (n > 0) {
jump summation(n - 1, s + n);
} else {
return(s);
}
}
foo()
{
(bits64 ret) = call summation(2000000, 0);
return(ret)
}
최적화되지 않은 버전에서 분리형2000000
(200만)으로 변경해 봤다.% ghc sum_opt.cmm -no-hs-main call_cmm.c -o sum_opt
% ./sum_opt
ret: 2000001000000
전혀 문제 없어요.그럼 2000000
(200만)이 아니라 1000000000
(10억)해보자.sum_opt.cmm
...
(bits64 ret) = call summation(1000000000, 0);
...
% ghc sum_opt.cmm -no-hs-main call_camm.c -o sum_opt
% ./sum_opt
ret: 500000000500000000
10억 번, 불러도 놀라지 않는다.총결산
고급 언어의 컴파일러를 만들 때 모든 하드웨어의 기계 언어는 생성하기 어렵다.이 경우 C 언어를 중간 언어로 사용하는 것은'아이쿠아'다.그러나 C 언어는 원래 그런 용도로 쓰이지 않아 문제가 많았다.그 문제에서 끝부분 회귀를 최적화하기 어려운 문제가 하나 있다.C - 고급 언어를 컴파일할 때 중간 표현으로 설계된 언어입니다.중간 표현으로 C-대 C의 장점은'컴백 마무리를 위한 최적화는 간단하다'다.처리 블록을 호출할 때
call
이런 방법이 아니라 jump
이런 방법을 준비했기 때문이다.간단한 예에서 C-의 꼬리 귀속 최적화 상황을 보여 준다.
Reference
이 문제에 관하여(C-후부 반복 최적화), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/yoshikuni_jujo/articles/tail-call-optimization-cmm텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)