함수형 프로그래밍-불변성은 비용이 많이 듭니까? [닫은]


98

문제는 두 부분으로 나뉩니다. 첫 번째는 개념입니다. 다음은 Scala에서 동일한 질문을보다 구체적으로 살펴 봅니다.

  1. 프로그래밍 언어에서 변경 불가능한 데이터 구조 만 사용하면 실제로 특정 알고리즘 / 로직을 구현하는 데 본질적으로 계산 비용이 더 많이 듭니까? 이것은 불변성이 순전히 기능적인 언어의 핵심 신조라는 사실을 이끌어냅니다. 이에 영향을 미치는 다른 요인이 있습니까?
  2. 좀 더 구체적인 예를 들어 보겠습니다. Quicksort 는 일반적으로 메모리 내 데이터 구조에서 변경 가능한 작업을 사용하여 학습되고 구현됩니다. 변경 가능한 버전과 비교 가능한 계산 및 저장 오버 헤드를 사용하여 PURE 기능적 방식으로 그러한 것을 어떻게 구현합니까? 특히 Scala에서. 아래에 몇 가지 조잡한 벤치 마크를 포함했습니다.

자세한 내용은:

저는 명령형 프로그래밍 배경 (C ++, Java)에서 왔습니다. 저는 함수형 프로그래밍, 특히 Scala를 탐구 해 왔습니다.

순수 함수형 프로그래밍의 몇 가지 기본 원칙 :

  1. 기능은 일류 시민입니다.
  2. 함수에는 부작용이 없으므로 객체 / 데이터 구조는 변경할 수 없습니다 .

현대의 JVM 은 객체 생성에 매우 효율적이고 가비지 수집 은 수명이 짧은 객체의 경우 매우 저렴하지만 객체 생성을 최소화하는 것이 더 낫습니다. 적어도 동시성과 잠금이 문제가되지 않는 단일 스레드 애플리케이션에서. Scala는 하이브리드 패러다임이므로 필요한 경우 변경 가능한 객체로 명령형 코드를 작성하도록 선택할 수 있습니다. 그러나 객체를 재사용하고 할당을 최소화하기 위해 수년을 보냈던 사람입니다. 나는 그것을 허용하지 않을 생각의 학교에 대해 잘 이해하고 싶습니다.

특정한 경우에 저는 이 튜토리얼 6 의이 코드 스 니펫에 약간 놀랐습니다 . Java 버전의 Quicksort와 깔끔하게 보이는 Scala 구현이 있습니다.

여기에 구현을 벤치마킹하려는 시도가 있습니다. 상세한 프로파일 링을하지 않았습니다. 그러나 제 생각에는 할당 된 개체 수가 선형 (재귀 호출 당 하나씩)이기 때문에 Scala 버전이 더 느립니다. 테일 콜 최적화가 작동 할 가능성이 있습니까? 내가 맞다면 Scala는 자체 재귀 호출에 대한 꼬리 호출 최적화를 지원합니다. 그래서 그것은 단지 그것을 돕고 있어야합니다. Scala 2.8을 사용하고 있습니다.

자바 버전

public class QuickSortJ {

    public static void sort(int[] xs) {
      sort(xs, 0, xs.length -1 );
    }

    static void sort(int[] xs, int l, int r) {
      if (r >= l) return;
      int pivot = xs[l];
      int a = l; int b = r;
      while (a <= b){
        while (xs[a] <= pivot) a++;
        while (xs[b] > pivot) b--;
        if (a < b) swap(xs, a, b);
      }
      sort(xs, l, b);
      sort(xs, a, r);
    }

    static void swap(int[] arr, int i, int j) {
      int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
    }
}

Scala 버전

object QuickSortS {

  def sort(xs: Array[Int]): Array[Int] =
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length / 2)
      Array.concat(
        sort(xs filter (pivot >)),
        xs filter (pivot ==),
        sort(xs filter (pivot <)))
    }
}

구현을 비교하기위한 스칼라 코드

import java.util.Date
import scala.testing.Benchmark

class BenchSort(sortfn: (Array[Int]) => Unit, name:String) extends Benchmark {

  val ints = new Array[Int](100000);

  override def prefix = name
  override def setUp = {
    val ran = new java.util.Random(5);
    for (i <- 0 to ints.length - 1)
      ints(i) = ran.nextInt();
  }
  override def run = sortfn(ints)
}

val benchImmut = new BenchSort( QuickSortS.sort , "Immutable/Functional/Scala" )
val benchMut   = new BenchSort( QuickSortJ.sort , "Mutable/Imperative/Java   " )

benchImmut.main( Array("5"))
benchMut.main( Array("5"))

결과

연속 5 회 실행에 대한 시간 (밀리 초)

Immutable/Functional/Scala    467    178    184    187    183
Mutable/Imperative/Java        51     14     12     12     12

10
순진하게 구현하거나 명령형 언어로 확장 된 메서드를 사용하면 비용이 많이 듭니다. 스마트 컴파일러 - (예를 들어, GHC, 하스켈 컴파일러와 하스켈이 유일한 불변의 값을) 할 수 불변성 수 라이벌 코드는 가변성을 사용하는 것이 achive 성능을 활용할 수 있습니다. 말할 필요도없이, 퀵소트의 순진한 구현은 다른 비용이 많이 드는 것들 중에서도 무거운 재귀와 O(n)목록 연결을 사용하기 때문에 여전히 개가 느립니다 . 하지만 의사 코드 버전보다 짧습니다.)

3
훌륭한 관련 블로그 기사는 다음과 같습니다. blogs.sun.com/jrose/entry/larval_objects_in_the_vm 은 Java와 기능적 VM 언어에 크게 도움이 될 것이기 때문에 그를 격려합니다
andersoj

2
이 SO 스레드에는 함수형 프로그래밍의 효율성에 대한 많은 자세한 논의가 있습니다. stackoverflow.com/questions/1990464/... . 내가 알고 싶었던 많은 답변을 제공합니다.
smartnut007 2010

5
여기서 가장 순진한 것은 벤치 마크입니다. 그런 코드로는 아무것도 벤치마킹 할 수 없습니다! 결론을 내리기 전에 JVM에 대한 벤치 마크 수행에 대한 기사를 진지하게 읽어야합니다. JVM이 실행하기 전에 아직 코드를 JIT하지 않았을 수도 있다는 사실을 알고 계셨습니까? 힙 초기 및 최대 크기를 적절하게 설정 했습니까 (JVM 프로세스가 더 많은 메모리를 요구하는 시간을 고려하지 않습니까?)? 어떤 메소드가 컴파일 또는 재 컴파일되는지 알고 있습니까? GC를 알고 있습니까? 이 코드에서 얻은 결과는 전혀 의미가 없습니다!
Bruno Reis

2
@userunknown 아니요, 선언적입니다. 명령형 프로그래밍은 "명령으로 상태를 변경"하는 반면, 함수형 프로그래밍은 "상태 변경을 방지"하는 "선언적 프로그래밍 패러다임"입니다 ( Wikipedia ). 그렇습니다. 기능과 명령은 완전히 다른 두 가지이며 작성한 코드는 필수 가 아닙니다 .
Brian McCutchon

답변:


106

여기에 몇 가지 오해 가 있기 때문에 몇 가지 요점을 명확히하고 싶습니다.

  • “in-place”quicksort는 실제로 in-place가 아닙니다 (그리고 quicksort는 정의에 의해 in-place 가 아닙니다 ). 재귀 적 단계를위한 스택 공간 형태의 추가 스토리지가 필요 합니다. 최상의 경우 O (log n ) , 최악의 경우 O ( n ) 순서입니다.

  • 배열에서 작동하는 Quicksort의 기능적 변형을 구현하면 목적을 달성 할 수 없습니다. 배열은 불변하지 않습니다.

  • Quicksort의 "적절한"기능 구현은 불변 목록을 사용합니다. 물론 제자리는 아니지만 절차 적 제자리 버전 과 동일한 최악의 점근 런타임 ( O ( n ^ 2)) 및 공간 복잡성 ( O ( n ))을 갖습니다.

    평균적으로 실행 시간은 현재 위치 변형 ( O ( n log n )) 의 실행 시간 과 동일합니다 . 그러나 공간 복잡도는 여전히 O ( n )입니다.


기능적 퀵 정렬 구현 에는 두 가지 명백한 단점 이 있습니다. 다음에서는 Haskell 도입부 에서 Haskell (나는 Scala를 모릅니다 ...)의 참조 구현을 고려해 보겠습니다 .

qsort []     = []
qsort (x:xs) = qsort lesser ++ [x] ++ qsort greater
    where lesser  = (filter (< x) xs)
          greater = (filter (>= x) xs)
  1. 첫 번째 단점 은 매우 유연하지 않은 피벗 요소를 선택 하는 것입니다. 최신 퀵 정렬 구현의 강점은 현명한 피벗 선택에 크게 의존합니다 ( Bentley 등의 "정렬 기능 엔지니어링" 비교 ). 위의 알고리즘은 그 점에서 열악하므로 평균 성능이 상당히 저하됩니다.

  2. 둘째,이 알고리즘은 O ( n ) 연산 인 목록 연결 (목록 생성 대신 )을 사용합니다. 이것은 점근 적 복잡성에 영향을주지 않지만 측정 가능한 요소입니다.

세 번째 단점은 다소 숨겨져 있습니다. "in-place"변형과 달리이 구현 은 목록의 cons 셀에 대해 힙에서 메모리를 지속적으로 요청 하고 잠재적으로 모든 곳에 메모리를 분산시킵니다. 결과적으로이 알고리즘은 캐시 지역성 이 매우 낮습니다 . 현대 함수형 프로그래밍 언어의 스마트 할당자가이 문제를 완화 할 수 있는지는 모르겠습니다.하지만 현대 컴퓨터에서는 캐시 미스가 주요 성능 킬러가되었습니다.


결론은 무엇입니까? 다른 사람들과 달리 퀵 정렬은 본질적으로 필수적이며 FP 환경에서 제대로 수행되지 않는 이유입니다. 오히려 나는 퀵 정렬이 기능적 알고리즘의 완벽한 예라고 주장합니다. 불변 환경으로 원활하게 변환되고 점근 적 실행 시간과 공간 복잡성이 절차 적 구현과 동등하며 절차 적 구현조차 재귀를 사용합니다.

그러나이 알고리즘 은 변경 불가능한 도메인으로 제한 될 때 여전히 성능이 저하됩니다. 그 이유는 알고리즘이 배열에서만 효율적으로 수행 할 수있는 많은 (때로는 낮은 수준의) 미세 조정의 이점을 누릴 수있는 독특한 속성을 가지고 있기 때문입니다. Quicksort에 대한 순진한 설명은 이러한 모든 복잡함을 놓치고 있습니다 (기능적 및 절차 적 변형 모두에서).

"정렬 기능 엔지니어링"을 읽은 후에는 더 이상 퀵 정렬을 우아한 알고리즘이라고 생각할 수 없습니다. 효율적으로 구현하면 엔지니어의 작품이 아니라 투박한 엉망입니다 (엔지니어링의 가치를 떨어 뜨리는 것이 아닙니다! 이것은 자체적 인 미학을 가지고 있습니다).


그러나 나는 또한이 점이 퀵 정렬에만 해당된다는 점을 지적하고 싶습니다. 모든 알고리즘이 동일한 종류의 저수준 조정을 수행 할 수있는 것은 아닙니다. 많은 알고리즘과 데이터 구조 가 불변 환경에서 성능 손실없이 실제로 표현 될 있습니다.

그리고 불변성 은 값 비싼 복사본이나 스레드 간 동기화의 필요성을 제거하여 성능 비용을 낮출 수도 있습니다 .

그래서 원래의 질문에 대답하기 위해“ 불변성은 비용이 많이 듭니까? ”– 퀵 정렬의 특별한 경우에는 실제로 불변성의 결과 인 비용이 있습니다. 그러나 일반적으로, 전혀 .


10
+1-좋은 답변입니다! 개인적으로는 안된다 기 보다는 가끔 . 그래도 그것은 단지 성격 일뿐입니다. 당신은 문제를 아주 잘 설명했습니다.
Rex Kerr

6
불변 값을 사용하는 적절한 구현은 명령형 버전과 달리 즉시 병렬화 할 수 있음을 추가해야합니다. 현대의 기술적 맥락에서 이것은 점점 더 중요 해지고 있습니다.
Raphael 2011 년

qsort lesser ++ (x : qsort greater)도움 이 얼마나 필요 합니까?
Solomon Ucko

42

함수형 프로그래밍의 벤치 마크로서 이것에는 많은 문제가 있습니다. 주요 내용은 다음과 같습니다.

  • boxed / unboxed해야 할 수있는 기본 요소를 사용하고 있습니다. 원시 객체를 래핑하는 오버 헤드를 테스트하려는 것이 아니라 불변성을 테스트하려는 것입니다.
  • 제자리 작업이 비정상적으로 효과적인 (그리고 그럴 가능성이있는) 알고리즘을 선택했습니다. 변경 가능하게 구현 될 때 더 빠른 알고리즘이 있음을 보여주고 싶다면 이것은 좋은 선택입니다. 그렇지 않으면 오해의 소지가 있습니다.
  • 잘못된 타이밍 기능을 사용하고 있습니다. 사용 System.nanoTime.
  • 벤치 마크가 너무 짧아서 JIT 컴파일이 측정 된 시간의 중요한 부분이 아니라고 확신 할 수 없습니다.
  • 배열은 효율적인 방식으로 분할되지 않습니다.
  • 배열은 변경 가능하므로 FP와 함께 사용하는 것은 어쨌든 이상한 비교입니다.

따라서이 비교는 고성능 코드를 작성하기 위해 언어 (및 알고리즘)를 자세히 이해해야하는 훌륭한 예시입니다. 그러나 FP와 비 FP의 비교는 그리 좋지 않습니다. 원한다면 Computer Languages ​​Benchmark Game에서 Haskell vs. C ++를 확인하십시오 . 집으로 가져가는 메시지는 패널티가 일반적으로 2 또는 3 배 정도라는 것입니다.하지만 실제로는 다릅니다. (Haskell 사람들이 가능한 가장 빠른 알고리즘을 작성했다는 약속은 없지만 적어도 그들 중 일부는 아마도 시도했을 것입니다! 그런 다음 Haskell 중 일부는 C 라이브러리를 호출합니다 ....)

이제 Quicksort의보다 합리적인 벤치 마크를 원한다고 가정하고 이것이 아마도 FP 대 가변 알고리즘의 최악의 경우 중 하나임을 인식하고 데이터 구조 문제를 무시합니다 (즉, 변경 불가능한 배열을 가질 수있는 척).

object QSortExample {
  // Imperative mutable quicksort
  def swap(xs: Array[String])(a: Int, b: Int) {
    val t = xs(a); xs(a) = xs(b); xs(b) = t
  }
  def muQSort(xs: Array[String])(l: Int = 0, r: Int = xs.length-1) {
    val pivot = xs((l+r)/2)
    var a = l
    var b = r
    while (a <= b) {
      while (xs(a) < pivot) a += 1
      while (xs(b) > pivot) b -= 1
      if (a <= b) {
        swap(xs)(a,b)
        a += 1
        b -= 1
      }
    }
    if (l<b) muQSort(xs)(l, b)
    if (b<r) muQSort(xs)(a, r)
  }

  // Functional quicksort
  def fpSort(xs: Array[String]): Array[String] = {
    if (xs.length <= 1) xs
    else {
      val pivot = xs(xs.length/2)
      val (small,big) = xs.partition(_ < pivot)
      if (small.length == 0) {
        val (bigger,same) = big.partition(_ > pivot)
        same ++ fpSort(bigger)
      }
      else fpSort(small) ++ fpSort(big)
    }
  }

  // Utility function to repeat something n times
  def repeat[A](n: Int, f: => A): A = {
    if (n <= 1) f else { f; repeat(n-1,f) }
  }

  // This runs the benchmark
  def bench(n: Int, xs: Array[String], silent: Boolean = false) {
    // Utility to report how long something took
    def ptime[A](f: => A) = {
      val t0 = System.nanoTime
      val ans = f
      if (!silent) printf("elapsed: %.3f sec\n",(System.nanoTime-t0)*1e-9)
      ans
    }

    if (!silent) print("Scala builtin ")
    ptime { repeat(n, {
      val ys = xs.clone
      ys.sorted
    }) }
    if (!silent) print("Mutable ")
    ptime { repeat(n, {
      val ys = xs.clone
      muQSort(ys)()
      ys
    }) }
    if (!silent) print("Immutable ")
    ptime { repeat(n, {
      fpSort(xs)
    }) }
  }

  def main(args: Array[String]) {
    val letters = (1 to 500000).map(_ => scala.util.Random.nextPrintableChar)
    val unsorted = letters.grouped(5).map(_.mkString).toList.toArray

    repeat(3,bench(1,unsorted,silent=true))  // Warmup
    repeat(3,bench(10,unsorted))     // Actual benchmark
  }
}

기능적 Quicksort에 대한 수정 사항을 확인하여 가능한 경우 데이터를 한 번만 살펴보고 기본 제공 정렬과 비교합니다. 실행하면 다음과 같은 결과가 나타납니다.

Scala builtin elapsed: 0.349 sec
Mutable elapsed: 0.445 sec
Immutable elapsed: 1.373 sec
Scala builtin elapsed: 0.343 sec
Mutable elapsed: 0.441 sec
Immutable elapsed: 1.374 sec
Scala builtin elapsed: 0.343 sec
Mutable elapsed: 0.442 sec
Immutable elapsed: 1.383 sec

따라서 자신의 정렬을 작성하는 것이 나쁜 생각이라는 것을 배우는 것 외에도 후자가 다소 신중하게 구현되면 변경 불가능한 퀵 정렬에 대해 ~ 3 배의 패널티가 있음을 발견했습니다. (작은 것, 같은 것, 피벗보다 큰 것의 세 가지 배열을 반환하는 trisect 메서드를 작성할 수도 있습니다. 이렇게하면 속도가 약간 더 빨라질 수 있습니다.)


권투 / 개봉에 관해서. 이것이 자바 측의 벌칙이어야한다면 오른쪽? Isnt Int는 Scala (vs Integer)의 기본 숫자 유형입니다. 따라서 스칼라 측에는 복싱 햅틱이 없습니다. 자동 복싱은 scala Int를 java.lang.Integer / int로 형성하기 때문에 복싱은 Java 측에서만 문제가됩니다. 여기에이 주제에 대해 자세히 설명하는 링크가 있습니다. ansorg-it.com/en/scalanews-001.html
smartnut007 2010

예, 저는 여기서 악마 옹호자 역할을합니다. 가변성은 퀵소트 디자인의 중간 부분입니다. 그래서 저는 문제에 대한 순수한 기능적 접근 방식에 대해 매우 궁금했습니다. 한숨, 나는 스레드 에서이 진술을 열 번째로 말했다 :-). 내가 일어나서 돌아올 때 나머지 게시물을 살펴볼 것입니다. 감사.
smartnut007 2010

2
@ smartnut007-Scala 컬렉션은 일반적입니다. 제네릭은 대부분 boxed 유형을 필요로합니다 (특정 기본 유형에 대해 특수화하려는 노력이 진행 중이지만). 따라서 멋진 컬렉션 메서드를 모두 사용할 수는 없으며이를 통해 기본 유형 컬렉션을 전달할 때 패널티가 없다고 가정합니다. 기본 유형은 들어가는 도중에 박스에 넣어야하고 나가는 도중에 풀어야 할 가능성이 큽니다.
Rex Kerr

나는 당신이 말한 가장 큰 결함이 단지 추측이라는 사실이 마음에 들지 않습니다 :-)
smartnut007

1
@ smartnut007-확인하기가 어렵고 사실이라면 결과를 망치기 때문에 가장 큰 결함입니다. 권투가 아니라고 확신하는 경우 결함이 유효하지 않다는 데 동의합니다. 결점은 복싱 이 있다는 것이 아니라 복싱 있는지 여부를 모른다는 것입니다 (그리고 저도 확실하지 않습니다. 전문화로 인해 알아 내기가 까다로워졌습니다). Java 측 (또는 Scala 가변 구현)에서는 기본 요소 만 사용하기 때문에 boxing이 없습니다. 어쨌든 불변 버전은 n log n 공간을 통해 작동하므로 비교 / 스왑 비용을 메모리 할당과 비교하게됩니다.
Rex Kerr

10

나는 당신이 사용하고 있기 때문에 Scala 버전이 실제로 꼬리 재귀라고 생각하지 않습니다 Array.concat.

또한 이것이 관용적 Scala 코드라고해서 이것이 최선의 방법이라는 의미는 아닙니다.

이를 수행하는 가장 좋은 방법은 Scala의 내장 정렬 기능 중 하나를 사용하는 것입니다. 그렇게하면 불변성 보장을 받고 빠른 알고리즘이 있다는 것을 알 수 있습니다.

Stack Overflow question Scala에서 배열을 어떻게 정렬합니까?를 참조하십시오 . 예를 들어.


4
또한 두 개의 재귀 호출을해야하므로 꼬리 재귀 빠른 정렬이 가능하지 않다고 생각합니다
Alex Lo

1
가능합니다. 연속 클로저를 사용하여 스택이 될 프레임을 힙으로 들어 올리면됩니다.
Brian

내장 된 scala.util.Sorting.quickSort (array)는 배열을 변경합니다. 당연히 자바만큼 빠르다. 효율적인 순수 기능 솔루션에 관심이 있습니다. 그렇지 않다면 왜. 일반적으로 스칼라 또는 기능적 패러다임의 한계입니까? 그런 것.
smartnut007 2010

@ smartnut007 : 어떤 버전의 Scala를 사용하고 있습니까? Scala 2.8에서는 array.sorted새로 정렬 된 배열을 반환하고 원래 배열을 변경하지 않는 방식으로 수행 할 수 있습니다 .
missingfaktor 2010

@AlexLo-꼬리 재귀 빠른 정렬이 가능합니다. 같은 것 :TAIL-RECURSIVE-QUICKSORT(Array A, int lo, int hi): while p < r: q = PARTITION(A, lo, hi); TAIL-RECURSIVE-QUICKSORT(A, lo, q - 1); p = q + 1;
Jakeway

8

불변성은 비싸지 않습니다. 프로그램이 수행해야하는 작업의 작은 하위 집합을 측정하고 퀵 정렬 측정과 같이 부팅 할 가변성을 기반으로하는 솔루션을 선택하면 비용이 많이들 수 있습니다.

간단히 말해, 순전히 기능적인 언어를 사용할 때 퀵 정렬을하지 않습니다.

이것을 다른 각도에서 생각해 봅시다. 다음 두 가지 기능을 고려해 보겠습니다.

// Version using mutable data structures
def tailFrom[T : ClassManifest](arr: Array[T], p: T => Boolean): Array[T] = {
  def posIndex(i: Int): Int = {
    if (i < arr.length) {
      if (p(arr(i)))
        i
      else
        posIndex(i + 1)
    } else {
      -1
    }
  }

  var index = posIndex(0)

  if (index < 0) Array.empty
  else {
    var result = new Array[T](arr.length - index)
    Array.copy(arr, index, result, 0, arr.length - index)
    result
  }
}

// Immutable data structure:

def tailFrom[T](list: List[T], p: T => Boolean): List[T] = {
  def recurse(sublist: List[T]): List[T] = {
    if (sublist.isEmpty) sublist
    else if (p(sublist.head)) sublist
    else recurse(sublist.tail)
  }
  recurse(list)
}

이를 벤치마킹하면 변경 가능한 데이터 구조를 사용하는 코드가 배열을 복사해야하기 때문에 성능이 훨씬 더 나쁘다는 것을 알 수 있지만 변경 불가능한 코드는 그 자체로 신경 쓸 필요가 없습니다.

변경 불가능한 데이터 구조로 프로그래밍 할 때 강점을 활용하도록 코드를 구조화합니다. 단순히 데이터 유형이나 개별 알고리즘이 아닙니다. 프로그램은 다른 방식으로 설계 됩니다.

그렇기 때문에 벤치마킹은 일반적으로 의미가 없습니다. 한 스타일 또는 다른 스타일에 자연스럽고 해당 스타일이이기는 알고리즘을 선택하거나 종종 비실용적 인 전체 응용 프로그램을 벤치마킹합니다.


7

배열 정렬은 우주에서 가장 중요한 작업과 같습니다. 많은 우아한 '불변'전략 / 구현이 '어레이 정렬'마이크로 벤치 마크에서 제대로 실패하는 것은 놀라운 일이 아닙니다. 그렇다고 불변성이 "일반적으로"비용이 많이 든다는 것을 의미하지는 않습니다. 변경 불가능한 구현이 변경 가능한 구현과 비교하여 수행되는 많은 작업이 있지만 배열 정렬은 종종 그중 하나가 아닙니다.


7

명령형 알고리즘과 데이터 구조를 기능적 언어로 간단히 다시 작성하는 경우 실제로 비용이 많이 들고 쓸모가 없습니다. 사물을 빛나게하려면 함수형 프로그래밍에서만 사용할 수있는 기능 (데이터 구조 지속성, 지연 평가 등)을 사용해야합니다.


Scala에서 구현을 제공하기에 충분히 친절 할 수 있습니까?
smartnut007 2010

3
powells.com/biblio/17-0521631246-0 (Chris Okasaki의 순수 기능 데이터 구조)-이 책을 살펴보십시오. 효과적인 알고리즘과 데이터 구조를 구현할 때 함수형 프로그래밍의 이점을 활용하는 것에 대한 강력한 이야기가 있습니다.
Vasil Remeniuk 2010

1
code.google.com/p/pfds Debashish Ghosh가 Scala에서 구현 한 일부 데이터 구조
Vasil Remeniuk 2010

Scala가 필수적이 아니라고 생각하는 이유를 설명해 주시겠습니까? list.filter (foo).sort (bar).take (10)-무엇이 더 시급 할 수 있습니까?
사용자 알 수없는

7

Scala 의 불변성 비용

다음은 Java보다 거의 빠른 버전입니다. ;)

object QuickSortS {
  def sort(xs: Array[Int]): Array[Int] = {
    val res = new Array[Int](xs.size)
    xs.copyToArray(res)
    (new QuickSortJ).sort(res)
    res
  }
}

이 버전은 배열의 복사본을 만들고 Java 버전을 사용하여 제자리에 정렬 한 다음 복사본을 반환합니다. Scala는 내부적으로 불변 구조를 사용하도록 강요하지 않습니다.

따라서 Scala의 이점은 적절하다고 판단되는대로 변경 가능성과 불변성을 활용할 수 있다는 것입니다. 단점은 잘못하면 불변성의 이점을 실제로 얻지 못한다는 것입니다.


이것은 질문에 대한 정확한 대답은 아니지만 좋은 대답의 일부라고 생각합니다. Quicksort는 가변 구조를 사용할 때 더 빠릅니다. 그러나 불변성의 가장 큰 장점은 인터페이스이며 적어도 스칼라에서는 둘 다 가질 수 있습니다. 퀵 정렬의 경우 변경이 더 빠르지 만 성능이 뛰어나고 대부분 변경 불가능한 코드를 작성하는 데 방해가되지는 않습니다.
Paul Draper 2015

7

QuickSort는 제자리에서 수행 할 때 더 빠른 것으로 알려져 있으므로 이것은 공정한 비교가 아닙니다!

그 말은 ... Array.concat? 다른 것이 없다면 명령형 프로그래밍에 최적화 된 컬렉션 유형이 함수 알고리즘에서 사용하려고 할 때 특히 느린 방법을 보여줍니다. 거의 모든 다른 선택이 더 빠를 것입니다!


고려해야 할 또 다른 매우 중요한 점 두 가지를 비교하는 것은 접근 할 때 가장 중요한 문제 : "여러 노드 / 코어에서이 규모를 얼마나 잘하는지"

불변의 퀵소트를 찾고 있다면 실제로 병렬 퀵소트를 원하기 때문에 그렇게 할 가능성이 있습니다. Wikipedia에는이 주제에 대한 인용이 있습니다. http://en.wikipedia.org/wiki/Quicksort#Parallelizations

스칼라 버전은 함수가 반복되기 전에 간단히 포크 할 수 있으므로 사용 가능한 코어가 충분한 경우 수십억 개의 항목이 포함 된 목록을 매우 빠르게 정렬 할 수 있습니다.

현재 내 시스템의 GPU에는 Scala 코드를 실행할 수만 있다면 128 개의 코어를 사용할 수 있습니다. 이것은 현재 세대보다 2 년 뒤진 단순한 데스크톱 시스템입니다.

단일 스레드 명령형 접근 방식과 비교하면 어떻게 될까요?

따라서 더 중요한 질문은 다음과 같습니다.

"개별 코어가 더 빨라지지 않고 동기화 / 잠금이 병렬화에 실질적인 도전이된다는 점을 감안할 때 가변성은 비용이 많이 듭니까?"


거기에 논쟁이 없습니다. Quicksort는 정의상 메모리 내 정렬입니다. 나는 대부분의 사람들이 대학에서 그것을 기억한다고 확신합니다. 그러나 순수한 기능적 방법으로 어떻게 빨리 정렬합니까? 즉, 부작용없이.
smartnut007 2010

그 중요한 원인은 기능적 패러다임이 부작용이있는 기능만큼 빠르다는 주장이 있습니다.
smartnut007

목록 버전은 시간을 절반으로 줄입니다. 그래도 자바 버전의 속도에 가까운 곳은 없습니다.
smartnut007

Scala가 필수적이 아니라고 생각하는 이유를 설명해 주시겠습니까? list.filter (foo).sort (bar).take (10)-무엇이 더 시급 할 수 있습니까? 감사.
사용자 알 수없는

@user unknown : 언급 된 예제가 나에게 잘 작동하는 것처럼 보이기 때문에 "명령 적"이 의미하는 바를 명확히 할 수 있습니다. Scala 자체는 명령 적이거나 선언적이지 않으며 언어는 두 스타일을 모두 지원하며 이러한 용어는 특정 예제를 설명하는 데 가장 적합합니다.
Kevin Wright

2

OO 프로그래밍은 복잡성을 숨기기 위해 추상화를 사용하고, 함수형 프로그래밍은 복잡성을 제거하기 위해 불변성을 사용한다고합니다. Scala의 하이브리드 세계에서 우리는 OO를 사용하여 명령형 코드를 숨길 수 있으며 응용 프로그램 코드는 현명하지 않습니다. 실제로 컬렉션 라이브러리는 명령형 코드를 많이 사용하지만 그렇다고해서 사용하지 말아야한다는 의미는 아닙니다. 다른 사람들이 말했듯이 조심스럽게 사용하면 두 세계의 장점을 모두 얻을 수 있습니다.


Scala가 필수적이 아니라고 생각하는 이유를 설명해 주시겠습니까? list.filter (foo).sort (bar).take (10)-무엇이 더 시급 할 수 있습니까? 감사.
사용자 알 수없는

그가 어디에서 스칼라가 필수가 아니라고 말했는지 모르겠습니다.
Janx
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.