왜 어디서나 게으른 평가가 사용되지 않습니까?


32

나는 게으른 평가가 어떻게 작동하는지 배웠는데 왜 현재 평가되는 모든 소프트웨어에 게으른 평가가 적용되지 않습니까? 여전히 열성적인 평가를 사용하는 이유는 무엇입니까?


2
변경 가능한 상태와 지연 평가를 혼합하면 발생할 수있는 예는 다음과 같습니다. alicebobandmallory.com/articles/2011/01/01/…
Jonas Elfström

2
@ JonasElfström : 변경 가능한 상태를 가능한 구현 중 하나와 혼동하지 마십시오. 가변 상태는 무한한 지연 스트림 값을 사용하여 구현할 수 있습니다. 그러면 가변 변수 문제가 없습니다.
조르지오

명령형 프로그래밍 언어에서 "게으른 평가"는 프로그래머의 의식적인 노력이 필요합니다. 명령형 언어로 된 일반 프로그래밍은 이것을 쉽게 만들었지 만 결코 투명하지는 않습니다. 이 질문의 반대편에 대한 답은 "왜 함수형 프로그래밍 언어가 모든 곳에서 사용되지 않습니까?"라는 또 다른 질문을 제시합니다.
rwong

2
함수형 프로그래밍 언어는 나사에 망치를 사용하지 않는 것과 같은 이유로 모든 곳에서 사용되는 것은 아니며, 모든 문제를 기능적 입력-> 출력 방식으로 쉽게 표현할 수있는 것은 아니며 GUI와 같이 명령형으로 표현하는 것이 더 적합합니다. .
ALXGTV

또한, 기능적 프로그래밍 언어 (또는 적어도 둘 다 기능적이라고 주장하는)의 두 클래스, 명령형 기능적 언어 (예 : Clojure, Scala) 및 선언적 (예 : Haskell, OCaml)이 있습니다.
ALXGTV

답변:


38

지연 평가에는 장부 관리 오버 헤드가 필요합니다. 아직 평가되지 않았는지 여부 등을 알아야합니다. 열성적인 평가는 항상 평가되므로 알 필요가 없습니다. 동시 상황에서 특히 그렇습니다.

두 번째로, 원하는 경우 나중에 호출 할 함수 객체로 패키징하여 열성적인 평가를 지연 평가로 변환하는 것은 쉽지 않습니다.

셋째, 게으른 평가는 통제력 상실을 의미합니다. 디스크에서 파일 읽기를 게으르게 평가하면 어떻게됩니까? 아니면 시간이 있습니까? 허용되지 않습니다.

열악한 평가는보다 효율적이고 제어 가능하며 게으른 평가로 간단하게 변환됩니다. 왜 게으른 평가를 원하십니까?


10
디스크에서 파일을 느리게 읽는 것은 실제로 정말 깔끔합니다. 대부분의 간단한 프로그램과 스크립트의 경우 Haskell 's readFile정확히 필요한 것입니다. 게다가, 게으른 평가에서 열성적인 평가로 전환하는 것은 사소한 일입니다.
Tikhon Jelvis 8

3
마지막 단락을 제외하고 모두 동의하십시오. 게으른 평가는 체인 작업이있을 때 더욱 효율적이며 실제로 데이터가 필요한시기를보다 잘 제어 할 수 있습니다.
texasbruce

4
functor 법률은 "통제력 상실"에 관해 귀하와 함께 한마디하고 싶습니다. 변경 불가능한 데이터 유형에서 작동하는 순수한 함수를 작성하는 경우 게으른 평가는 신의 선물입니다. 하스켈과 같은 언어는 기본적으로 게으름 개념에 기초합니다. 특히 "안전하지 않은"코드와 혼합 될 때 일부 언어에서는 번거롭지 만 게으름이 위험하거나 기본적으로 나쁜 것처럼 들립니다. 위험한 코드에서는 "위험한"것입니다.
sara may

1
@DeadMG 코드의 종료 여부에 관심이 없다면 ... head [1 ..]Haskell에서 제공하는 코드는 1무엇입니까?
세미콜론

1
많은 언어에서 게으른 평가를 구현하면 최소한 복잡성이 발생합니다. 때로는 복잡성이 필요하고 지연 평가를 수행하면 전반적인 효율성이 향상됩니다. 특히 평가 대상이 조건부로만 필요한 경우에는 더욱 그렇습니다. 그러나 잘못 수행하면 코드를 작성할 때 잘못된 가정으로 인해 미묘한 버그가 발생하거나 성능 문제를 설명하기가 어려울 수 있습니다. 절충이 있습니다.
Berin Loritsch

17

주로 게으른 코드와 상태가 잘못 혼합되어 버그를 찾기가 어려울 수 있기 때문입니다. 종속 개체의 상태가 변경되면 평가할 때 지연 개체의 값이 잘못 될 수 있습니다. 상황이 적절하다는 것을 알면 프로그래머가 게으른 객체를 명시 적으로 코딩하는 것이 훨씬 좋습니다.

참고로 Haskell은 모든 것에 Lazy 평가를 사용합니다. 이것은 기능적 언어이고 상태를 사용하지 않기 때문에 가능합니다 (명백하게 표시된 예외적 인 상황 제외).


네, 변경 가능한 상태 + 게으른 평가 = 죽음. SICP 결승전에서 잃어버린 유일한 점 set!은 게으른 Scheme 인터프리터에서 사용 하는 것입니다. > :(
Tikhon Jelvis

3
"게으른 코드와 상태가 잘못 혼합 될 수 있습니다": 실제로 상태를 구현하는 방법에 따라 다릅니다. 공유 가변 변수를 사용하여 구현하고 상태의 일관성을 유지하기 위해 평가 순서에 의존하면 옳습니다.
조르지오

14

게으른 평가가 항상 좋은 것은 아닙니다.

게으른 평가의 성능 이점은 클 수 있지만 열악한 환경에서 대부분의 불필요한 평가를 피하는 것은 어렵지 않습니다. 반드시 게으른 것이 쉽고 완벽하게 이루어 지지만 코드에서 불필요한 평가는 거의 문제가되지 않습니다.

게으른 평가의 장점은보다 명확한 코드를 작성할 수 있다는 것입니다. 무한 자연수 목록을 필터링하여 10 번째 소수를 얻는 것과 해당 목록의 10 번째 요소를 취하는 것은 가장 간결하고 명확한 진행 방법 중 하나입니다 (의사 코드).

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

게으르지 않고 간결하게 표현하는 것은 매우 어렵다고 생각합니다.

그러나 게으름은 모든 것에 대한 답이 아닙니다. 우선 상태의 경우 게으름을 투명하게 적용 할 수 없으며 상태가 매우 명시 적 인 경우 Haskell에서 작업하지 않는 한 상태 저장을 자동으로 감지 할 수 없다고 생각합니다. 따라서 대부분의 언어에서 게으름은 수동으로 수행해야하므로 일이 덜 명확 해지고 게으른 평가의 큰 이점 중 하나가 제거됩니다.

게다가 게으름은 평가되지 않은 표현을 유지하는 데 상당한 오버 헤드가 발생하기 때문에 성능상의 단점이 있습니다. 스토리지를 모두 사용하며 단순한 값보다 작업 속도가 느립니다. 게으른 버전은 개 속도가 느리고 성능에 대해 추론하기가 어렵 기 때문에 코드를 간절히 찾아야한다는 사실은 드문 일이 아닙니다.

그것이 일어나는 경향이 있기 때문에 절대적인 최선의 전략은 없습니다. 게으름은 무한한 데이터 구조 나 다른 전략을 사용하여 더 나은 코드를 작성할 수 있다면 훌륭하지만, 최적화하기가 더 쉬울 수 있습니다.


A에 대한이 가능할 것이다 정말 똑똑한 컴파일러가 크게 오버 헤드를 완화 할 수 있습니다. 또는 추가 최적화를 위해 게으름을 활용합니까?
Tikhon Jelvis 8:12에

3

간절하고 게으른 평가의 장단점을 간단히 비교하면 다음과 같습니다.

  • 열성적인 평가 :

    • 불필요하게 평가하는 물건의 잠재적 인 오버 헤드.

    • 방해받지 않고 빠른 평가.

  • 게으른 평가 :

    • 불필요한 평가가 없습니다.

    • 값을 사용할 때마다 부기 오버 헤드가 발생합니다.

따라서 평가할 필요가없는 표현이 많으면 게으른 것이 좋습니다. 그러나 평가할 필요가없는 표현식이없는 경우 게으른 것은 순수한 오버 헤드입니다.

이제, 실제 소프트웨어를 살펴 수 있습니다 : 얼마나 많은 기능 당신이 할 쓰기 것을 하지 모든 인수의 평가를 필요로? 특히 한 가지만 수행하는 현대적인 짧은 기능의 경우 기능의 비율이이 범주에 속합니다. 따라서 게으른 평가는 실제로 아무것도 저장하지 않고 부기 오버 헤드를 대부분 발생시킵니다.

결과적으로 게으른 평가는 단순히 평균을 지불하지 않으며, 열성적인 평가는 현대 코드에 더 적합합니다.


1
"값을 사용할 때마다 부기 오버 헤드": 부기 오버 헤드가 Java와 같은 언어에서 null 참조를 확인하는 것보다 크다고 생각하지 않습니다. 두 경우 모두 1 비트의 정보 (평가 / 대기 대 널 / 널이 아님)를 확인해야하며 값을 사용할 때마다 수행해야합니다. 따라서 오버 헤드가 있지만 최소한입니다.
조르지오

1
"당신이 작성한 함수 중 몇 개가 모든 인수를 평가할 필요는 없습니까?": 이것은 단지 하나의 예제 응용 프로그램입니다. 재귀적이고 무한한 데이터 구조는 어떻습니까? 열성적인 평가로 구현할 수 있습니까? 반복자를 사용할 수 있지만 솔루션이 항상 간결한 것은 아닙니다. 물론 당신은 당신이 광범위하게 사용할 기회가 없었던 것을 놓치지 않을 것입니다.
조르지오

2
"결과적으로 게으른 평가는 단순히 평균을 지불하지 않으며, 열성적인 평가는 최신 코드에 더 적합합니다."
조르지오

1
@Giorgio 오버 헤드는 그리 많지 않을 수도 있지만, 조건부 (conditional)는 현대 CPU가 겪고있는 것 중 하나입니다. 잘못 예측 한 브랜치는 일반적으로 완전한 파이프 라인 플러시를 강요하여 10 개 이상의 CPU 사이클의 작업을 버립니다. 내부 루프에서 불필요한 조건을 원하지 않습니다. 함수 인수 당 10 사이클을 추가로 지불하는 것은 Java로 코딩하는 것만 큼 성능에 민감한 코드에는 거의 수용 할 수 없습니다. 게으른 평가를 통해 열성적인 평가로는 쉽게 할 수없는 몇 가지 트릭을 풀 수 있습니다. 그러나 대부분의 코드에는 이러한 트릭이 필요하지 않습니다.
cmaster

2
이것은 게으른 평가를 가진 언어에 대한 경험이 부족한 것 같습니다. 예를 들어, 무한 데이터 구조는 어떻습니까?
Andres F.

3

@DeadMG가 지적한 것처럼 지연 평가에는 예약 유지 오버 헤드가 필요합니다. 이것은 열악한 평가에 비해 비쌀 수 있습니다. 이 진술을 고려하십시오 :

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

계산하려면 약간의 계산이 필요합니다. 지연 평가를 사용하는 경우 평가를 사용할 때마다 평가되었는지 확인해야합니다. 이것이 많이 사용되는 타이트 루프 안에 있으면 오버 헤드가 크게 증가하지만 이점은 없습니다.

열성적인 평가와 괜찮은 컴파일러로 수식은 컴파일 타임에 계산됩니다. 대부분의 옵티마이 저는 해당되는 경우 루프에서 할당을 이동합니다.

지연 평가는 자주 액세스하지 않고 검색해야하는 오버 헤드가 높은 데이터를로드하는 데 가장 적합합니다. 따라서 핵심 기능보다 사례를 처리하는 것이 더 적합합니다.

일반적으로 가능한 빨리 액세스하는 항목을 평가하는 것이 좋습니다. 이 평가에서는 지연 평가가 작동하지 않습니다. 항상 무언가에 액세스 할 경우 게으른 평가는 오버 헤드를 추가하는 것입니다. 지연 평가 사용의 비용 / 혜택은 액세스하는 항목에 액세스 할 가능성이 낮아짐에 따라 감소합니다.

항상 게으른 평가를 사용하는 것도 초기 최적화를 의미합니다. 이것은 종종 더 복잡하고 비싼 코드를 초래하는 나쁜 습관입니다. 불행히도, 조기 최적화는 종종 간단한 코드보다 성능이 느린 코드를 생성합니다. 최적화 효과를 측정 할 수있을 때까지 코드를 최적화하는 것은 좋지 않습니다.

조기 최적화를 피하는 것은 좋은 코딩 방법과 충돌하지 않습니다. 모범 사례가 적용되지 않은 경우 초기 최적화는 루프 밖으로 계산을 이동하는 등의 우수한 코딩 사례를 적용하는 것으로 구성 될 수 있습니다.


1
당신은 경험이 부족하다고 주장하는 것 같습니다. Wadler의 "Why Functional Programming Matters"논문을 읽는 것이 좋습니다. 지연 평가 의 이유 를 설명하는 주요 섹션을 다룹니다 (힌트 : 성능, 초기 최적화 또는 "빈번히 액세스하지 않는 데이터로드"및 모듈 성과 관련된 모든 것)과는 거의 관련이 없습니다.
Andres F.

@AndresF 나는 당신이 참조하는 논문을 읽었습니다. 그러한 경우에 지연 평가 사용에 동의합니다. 초기 평가는 적절하지 않지만 추가 이동을 쉽게 추가 할 수 있으면 선택한 이동에 대한 하위 트리를 반환하면 큰 이점이있을 수 있다고 주장합니다. 그러나 해당 기능을 구축하는 것은 조기 최적화가 될 수 있습니다. 함수형 프로그래밍 외부에서는 지연 평가 사용 및 지연 평가 사용 실패와 관련하여 중요한 문제가 있습니다. 함수형 프로그래밍에서 지연 평가로 인해 상당한 성능 비용이 발생한다는보고가 있습니다.
BillThor

2
같은? 열성적인 평가를 사용할 때 상당한 성능 비용이보고됩니다 (필요하지 않은 평가 및 프로그램 비 종료 형태의 비용). 거의 모든 다른 사용 된 기능에는 비용이 있습니다. 모듈 자체는 비용이 많이들 수 있습니다. 문제는 가치가 있는지 여부입니다.
Andres F.

3

식의 가치를 결정하기 위해 식을 완전히 평가해야하는 경우 게으른 평가는 단점이 될 수 있습니다. 부울 값의 긴 목록이 있고 모든 값 이 true 인지 확인하려고 합니다.

[True, True, True, ... False]

이를 위해서는 목록의 모든 요소 를 살펴 봐야 합니다. 따라서 평가를 지연시킬 가능성은 없습니다. 접기를 사용하여 목록의 모든 부울 값이 참인지 확인할 수 있습니다. 지연 평가를 사용하는 접기 권한을 사용하면 목록의 모든 요소를 ​​살펴 봐야하기 때문에 지연 평가의 이점을 얻지 못합니다.

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

이 경우 폴드 오른쪽은 느린 폴드 왼쪽보다 훨씬 느리며 지연 평가를 사용하지 않습니다.

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

그 이유는 왼쪽으로 접는 부분이 꼬리 재귀를 사용하기 때문입니다. 즉, 반환 값을 누적하고 대량의 작업 체인을 작성하여 메모리에 저장하지 않습니다. 어쨌든 두 함수가 전체 목록을보아야하고 fold right는 tail recursion을 사용할 수 없기 때문에 lazy fold right보다 훨씬 빠릅니다. 요점은, 당신은 당면한 작업에 가장 적합한 것을 사용해야한다는 것입니다.


"그래서 요점은, 당면한 작업에 가장 적합한 것을 사용해야한다는 것입니다." +1
조르지오
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.