openlang

이 CPS 합계는 왜 Debug에서만 스택이 넘칠까요?

제가 이해한 게 맞는지 봐 주실 수 있을까요? 연습 삼아 짠 continuation-passing 방식 합계예요.

let rec sumK (xs: int list) (k: int -> int) =
    match xs with
    | [] -> k 0
    | x :: rest -> sumK rest (fun s -> k (s + x))

let xs = List.replicate 1_000_000 1
printfn "%d" (sumK xs id)

dotnet run -c Release는 1000000을 출력해요. 그냥 dotnet run, 즉 Debug는 스택 오버플로로 죽어요. 같은 소스, 같은 기기예요. 안의 호출이 전부 꼬리 위치라서 꼬리 호출 문제 같은데, 빌드의 어느 부분이 그걸 정하는지는 모르겠어요. Debug에서 뭔가를 켜는 게 정직한 해결일 수도 있고, 코드가 거기에 기대지 않게 고쳐야 할 수도 있어요.

영어에서 번역원문 보기

이어지는 대화 5

서로의 근거를 따라
  1. 고유 링크

    Debug 빌드는 꼬리 호출이 꺼져 있고 Release는 켜져 있습니다. sumK가 자기 자신을 부르는 호출은 어느 쪽이든 점프로 컴파일되므로 체인을 만드는 단계는 괜찮습니다. 넘치는 곳은 k (s + x), 꼬리 위치에서 클로저를 거치는 호출입니다. 꼬리 호출이 꺼지면 이 호출은 tail. 접두사 없는 평범한 callvirt가 되고, 마지막에 k 0이 실행될 때 클로저 백만 개가 각각 프레임을 쌓습니다. 스위치는 프로젝트의 Tailcalls 속성, 명령줄에서는 --tailcalls입니다.

    영어에서 번역원문 보기
  2. 고유 링크

    그러니까 같은 텍스트가 한 설정에서는 돌아가는 프로그램이고 다른 설정에서는 충돌이네요. 꼬리 호출 보장이 빌드 층에 있는 셈인데, 프로그램의 의미가 달린 것치고는 이상한 층이에요. [<TailCall>] 특성이 제대로 된 층에서의 해결로 보여요. sumK에 붙이면 요구가 소스에 적히니까 Debug 빌드도 접두사를 내보내야 해요.

    영어에서 번역원문 보기
  3. 고유 링크

    저도 그러길 바라서 제일 먼저 해 봤어요. [<TailCall>]은 검사만 더해요. 표시한 함수의 재귀 호출이 꼬리 위치가 아니면 컴파일러가 경고를 내고, 내보내는 코드는 그대로예요. sumK에서는 재귀 호출이 이미 꼬리 위치라서 조용하고, Debug 빌드는 전과 똑같이 넘쳐요. 게다가 문제가 되는 클로저는 표시한 함수도 아니에요.

    영어에서 번역원문 보기
  4. 고유 링크

    Debug에서 끄는 데 습관 말고 이유가 있나요? 제 짐작은 호출 스택이에요. 꼬리 호출로 대체된 프레임은 사라지니까 디버거나 예외 스택 트레이스에서 그 단계가 빠져요. Debug를 켜는 이유가 바로 그 화면이고요. 그게 이유라면 Debug에서 Tailcalls를 켜는 건 그 화면 일부를 내주고 돌아가는 프로그램을 얻는 거래라서, 어디서나 바꿀 기본값이라기보다 프로젝트마다 정할 일 같아요.

    영어에서 번역원문 보기
  5. 고유 링크

    맞는 얘기 같아요. 이 프로젝트는 프로그램 자체가 꼬리 호출에 기대고 있어서 두 설정 모두 켜 두었고, 이제 Debug에서도 1000000이 출력돼요.

    <PropertyGroup>
      <Tailcalls>true</Tailcalls>
    </PropertyGroup>

    라이브러리라면 sumK를 접두사가 필요 없게 다시 쓰는 편이 나을지도 몰라요. 빌드 설정은 사용하는 쪽이 고르니까요.

    영어에서 번역원문 보기

같은 질문의 다른 갈래

  1. CTFE는 선언이 요구하는 곳에서만 일어난다

    D의 CTFE를 한동안 잘못 그리고 있었어요. 함수가 컴파일 타임 평가에 적합하면, 인자를 아는 곳 어디서든 컴파일러가 알아서 평가해 줄 거라고 생각했거든요. 실제로는 컴파일 타임 값이 꼭 필요한 곳에서만 해요. 이 코드는…

    답글 4d
  2. 같은 Int, 두 타깃, 두 개의 답

    같은 Gleam 모듈을 두 타깃에서 돌리다가 발견했는데, 언어보다는 런타임 쪽 사실이에요. gleam run은 9007199254740993을 출력해요. gleam run --target javascript는 90071992…

    답글 3gleam
  3. expr를 감싸는 중괄호가 바이트코드 컴파일러가 보는 것을 정한다

    중괄호 없는 expr는 할 수만 있다면 Tcl에서 금지하고 싶은 습관이에요! 스타일 지적처럼 보이지만 전혀 아니에요. 중괄호 버전은 0을 찍어요. 바이트코드 컴파일러가 $qty == 3을 식으로 보고, 변수를 한 번 읽고,…

    답글 16tcl