C ++에 비해 D는 얼마나 빠릅니까? 10; typedef int value_type; typedef long long result_type; typedef std::vector<value_type>

D의 일부 기능이 마음에 들지만 런타임 패널티가 있다면 관심이 있습니까?

비교하기 위해 C ++과 D에서 많은 짧은 벡터의 스칼라 곱을 계산하는 간단한 프로그램을 구현했습니다. 결과는 놀랍습니다.

  • D : 18.9 초 [최종 런타임은 아래 참조]
  • C ++ : 3.8 초

C ++이 실제로 거의 5 배나 빠르거나 D 프로그램에서 실수를 했습니까?

나는 최근 리눅스 데스크탑에서 g ++ -O3 (gcc-snapshot 2011-02-19) 및 Dmd -O (dmd 2.052)로 C ++를 컴파일했습니다. 결과는 여러 번의 실행에서 재현 가능하고 표준 편차는 무시할 수 있습니다.

여기에 C ++ 프로그램이 있습니다 :

#include <iostream>
#include <random>
#include <chrono>
#include <string>

#include <vector>
#include <array>

typedef std::chrono::duration<long, std::ratio<1, 1000>> millisecs;
template <typename _T>
long time_since(std::chrono::time_point<_T>& time) {
      long tm = std::chrono::duration_cast<millisecs>( std::chrono::system_clock::now() - time).count();
  time = std::chrono::system_clock::now();
  return tm;
}

const long N = 20000;
const int size = 10;

typedef int value_type;
typedef long long result_type;
typedef std::vector<value_type> vector_t;
typedef typename vector_t::size_type size_type;

inline value_type scalar_product(const vector_t& x, const vector_t& y) {
  value_type res = 0;
  size_type siz = x.size();
  for (size_type i = 0; i < siz; ++i)
    res += x[i] * y[i];
  return res;
}

int main() {
  auto tm_before = std::chrono::system_clock::now();

  // 1. allocate and fill randomly many short vectors
  vector_t* xs = new vector_t [N];
  for (int i = 0; i < N; ++i) {
    xs[i] = vector_t(size);
      }
  std::cerr << "allocation: " << time_since(tm_before) << " ms" << std::endl;

  std::mt19937 rnd_engine;
  std::uniform_int_distribution<value_type> runif_gen(-1000, 1000);
  for (int i = 0; i < N; ++i)
    for (int j = 0; j < size; ++j)
      xs[i][j] = runif_gen(rnd_engine);
  std::cerr << "random generation: " << time_since(tm_before) << " ms" << std::endl;

  // 2. compute all pairwise scalar products:
  time_since(tm_before);
  result_type avg = 0;
  for (int i = 0; i < N; ++i)
    for (int j = 0; j < N; ++j)
      avg += scalar_product(xs[i], xs[j]);
  avg = avg / N*N;
  auto time = time_since(tm_before);
  std::cout << "result: " << avg << std::endl;
  std::cout << "time: " << time << " ms" << std::endl;
}

그리고 여기 D 버전 :

import std.stdio;
import std.datetime;
import std.random;

const long N = 20000;
const int size = 10;

alias int value_type;
alias long result_type;
alias value_type[] vector_t;
alias uint size_type;

value_type scalar_product(const ref vector_t x, const ref vector_t y) {
  value_type res = 0;
  size_type siz = x.length;
  for (size_type i = 0; i < siz; ++i)
    res += x[i] * y[i];
  return res;
}

int main() {
  auto tm_before = Clock.currTime();

  // 1. allocate and fill randomly many short vectors
  vector_t[] xs;
  xs.length = N;
  for (int i = 0; i < N; ++i) {
    xs[i].length = size;
  }
  writefln("allocation: %i ", (Clock.currTime() - tm_before));
  tm_before = Clock.currTime();

  for (int i = 0; i < N; ++i)
    for (int j = 0; j < size; ++j)
      xs[i][j] = uniform(-1000, 1000);
  writefln("random: %i ", (Clock.currTime() - tm_before));
  tm_before = Clock.currTime();

  // 2. compute all pairwise scalar products:
  result_type avg = cast(result_type) 0;
  for (int i = 0; i < N; ++i)
    for (int j = 0; j < N; ++j)
      avg += scalar_product(xs[i], xs[j]);
  avg = avg / N*N;
  writefln("result: %d", avg);
  auto time = Clock.currTime() - tm_before;
  writefln("scalar products: %i ", time);

  return 0;
}


답변

모든 최적화를 활성화하고 모든 안전 점검을 비활성화하려면 다음 DMD 플래그를 사용하여 D 프로그램을 컴파일하십시오.

-O -inline -release -noboundscheck

편집 : g ++, dmd 및 gdc로 프로그램을 시도했습니다. dmd는 뒤쳐 지지만 gdc는 g ++에 매우 가까운 성능을 달성합니다. 내가 사용한 명령 줄은 gdmd -O -release -inlinegdmd가 ddc 옵션을 허용하는 gdc를 감싸는 래퍼입니다.

어셈블러 목록을 보면 dmd 나 gdc inlined처럼 보이지 scalar_product않지만 g ++ / gdc는 MMX 명령을 내렸 으므로 루프를 자동 벡터화 할 수 있습니다.


답변

D 속도를 늦추는 것 중 하나는 하위 파 가비지 수집 구현입니다. GC에 큰 부담을주지 않는 벤치 마크는 동일한 컴파일러 백엔드로 컴파일 된 C 및 C ++ 코드와 매우 유사한 성능을 보여줍니다. GC에 큰 스트레스를주는 벤치 마크는 D가 심하게 작음을 보여줍니다. 그러나 이는 느리게 구현 된 보장이 아니라 단일 (심각한) 구현 품질 문제입니다. 또한 D를 사용하면 성능이 중요하지 않은 코드에서 GC를 선택 해제하고 메모리 관리를 조정하여 성능이 중요하지 않은 코드의 95 %에서 메모리를 계속 사용할 수 있습니다.

나는 한 최근 GC 성능을 향상에 어떤 노력을 하고 그 결과 적어도 합성 벤치 마크에서, 오히려 극적이었다. 이러한 변경 사항이 다음 몇 가지 릴리스 중 하나에 통합되어 문제가 완화되기를 바랍니다.


답변

OP와 도우미의 모든 작업에 감사드립니다.

한 가지 참고 사항-이 테스트는 추상화 / 기능 페널티 또는 백엔드 품질에 대한 일반적인 질문을 평가하지 않습니다. 사실상 하나의 최적화 (루프 최적화)에 중점을 둡니다. 나는 gcc의 백엔드가 dmd의 백엔드보다 약간 더 세련되었다고 말하는 것이 공평하다고 생각하지만, 그들 사이의 간격이 모든 작업에 대해 크다고 가정하는 것은 실수 일 것입니다.


답변

분명히 구현 품질 문제처럼 보입니다.

OP 코드로 몇 가지 테스트를 실행하고 약간 변경했습니다. 실제로 배열을 동적으로 할당 해야 한다는 가정 ( xs및 관련 스칼라)을 사용 하여 LDC / clang ++에 대해 D가 더 빨라졌습니다 . 일부 숫자는 아래를 참조하십시오.

OP에 대한 질문

C에 대한 반복마다 동일한 시드를 사용하고 D에는 그렇지 않은 것이 의도적 인 것입니까?

설정

scalar.d플랫폼간에 이식성을 갖도록 원본 D 소스 ( )를 조정했습니다 . 여기에는 배열의 크기에 액세스하고 수정하는 데 사용되는 숫자 유형 만 변경하는 것이 포함되었습니다.

그 후에 다음과 같이 변경했습니다.

  • uninitializedArrayxs의 스칼라에 대한 기본 초기화를 피하는 데 사용 됩니다 (아마도 가장 큰 차이를 만들었 음). D는 일반적으로 모든 것을 자동으로 초기화하지만 C ++에서는 그렇지 않기 때문에 중요합니다.

  • 코드를 인쇄에서 고려 교체 writefln와 함께writeln

  • 선택적 가져 오기로 변경
  • ^^평균 계산의 마지막 단계에 수동 곱셈 대신 pow 연산자 ( )를 사용했습니다.
  • 를 제거하고 size_typeindex_type별칭으로 적절하게 교체

… 따라서 scalar2.cpp( pastebin )이 발생합니다.

    import std.stdio : writeln;
    import std.datetime : Clock, Duration;
    import std.array : uninitializedArray;
    import std.random : uniform;

    alias result_type = long;
    alias value_type = int;
    alias vector_t = value_type[];
    alias index_type = typeof(vector_t.init.length);// Make index integrals portable - Linux is ulong, Win8.1 is uint

    immutable long N = 20000;
    immutable int size = 10;

    // Replaced for loops with appropriate foreach versions
    value_type scalar_product(in ref vector_t x, in ref vector_t y) { // "in" is the same as "const" here
      value_type res = 0;
      for(index_type i = 0; i < size; ++i)
        res += x[i] * y[i];
      return res;
    }

    int main() {
      auto tm_before = Clock.currTime;
      auto countElapsed(in string taskName) { // Factor out printing code
        writeln(taskName, ": ", Clock.currTime - tm_before);
        tm_before = Clock.currTime;
      }

      // 1. allocate and fill randomly many short vectors
      vector_t[] xs = uninitializedArray!(vector_t[])(N);// Avoid default inits of inner arrays
      for(index_type i = 0; i < N; ++i)
        xs[i] = uninitializedArray!(vector_t)(size);// Avoid more default inits of values
      countElapsed("allocation");

      for(index_type i = 0; i < N; ++i)
        for(index_type j = 0; j < size; ++j)
          xs[i][j] = uniform(-1000, 1000);
      countElapsed("random");

      // 2. compute all pairwise scalar products:
      result_type avg = 0;
      for(index_type i = 0; i < N; ++i)
        for(index_type j = 0; j < N; ++j)
          avg += scalar_product(xs[i], xs[j]);
      avg /= N ^^ 2;// Replace manual multiplication with pow operator
      writeln("result: ", avg);
      countElapsed("scalar products");

      return 0;
    }

테스트 후 scalar2.d(속도 최적화를 우선 순위로 한) 호기심으로 루프 mainforeach동등한 것으로 대체하고 scalar3.d( pastbin ) 이라고했습니다 .

    import std.stdio : writeln;
    import std.datetime : Clock, Duration;
    import std.array : uninitializedArray;
    import std.random : uniform;

    alias result_type = long;
    alias value_type = int;
    alias vector_t = value_type[];
    alias index_type = typeof(vector_t.init.length);// Make index integrals portable - Linux is ulong, Win8.1 is uint

    immutable long N = 20000;
    immutable int size = 10;

    // Replaced for loops with appropriate foreach versions
    value_type scalar_product(in ref vector_t x, in ref vector_t y) { // "in" is the same as "const" here
      value_type res = 0;
      for(index_type i = 0; i < size; ++i)
        res += x[i] * y[i];
      return res;
    }

    int main() {
      auto tm_before = Clock.currTime;
      auto countElapsed(in string taskName) { // Factor out printing code
        writeln(taskName, ": ", Clock.currTime - tm_before);
        tm_before = Clock.currTime;
      }

      // 1. allocate and fill randomly many short vectors
      vector_t[] xs = uninitializedArray!(vector_t[])(N);// Avoid default inits of inner arrays
      foreach(ref x; xs)
        x = uninitializedArray!(vector_t)(size);// Avoid more default inits of values
      countElapsed("allocation");

      foreach(ref x; xs)
        foreach(ref val; x)
          val = uniform(-1000, 1000);
      countElapsed("random");

      // 2. compute all pairwise scalar products:
      result_type avg = 0;
      foreach(const ref x; xs)
        foreach(const ref y; xs)
          avg += scalar_product(x, y);
      avg /= N ^^ 2;// Replace manual multiplication with pow operator
      writeln("result: ", avg);
      countElapsed("scalar products");

      return 0;
    }

LDC가 성능 측면에서 D 컴파일에 가장 적합한 옵션 인 것처럼 보이기 때문에 LLVM 기반 컴파일러를 사용하여 이러한 각 테스트를 컴파일했습니다. x86_64 Arch Linux 설치에서 다음 패키지를 사용했습니다.

  • clang 3.6.0-3
  • ldc 1:0.15.1-4
  • dtools 2.067.0-2

다음 명령을 사용하여 각각을 컴파일했습니다.

  • C ++ : clang++ scalar.cpp -o"scalar.cpp.exe" -std=c++11 -O3
  • 디: rdmd --compiler=ldc2 -O3 -boundscheck=off <sourcefile>

결과

각 소스 버전의 결과 ( 원시 콘솔 출력 스크린 샷 )는 다음과 같습니다.

  1. scalar.cpp (원래 C ++) :

    allocation: 2 ms
    
    random generation: 12 ms
    
    result: 29248300000
    
    time: 2582 ms

    C ++는 표준을 2582ms로 설정합니다 .

  2. scalar.d (수정 된 OP 소스) :

    allocation: 5 ms, 293 μs, and 5 hnsecs
    
    random: 10 ms, 866 μs, and 4 hnsecs
    
    result: 53237080000
    
    scalar products: 2 secs, 956 ms, 513 μs, and 7 hnsecs 

    이것은 ~ 2957 ms 동안 실행되었습니다 . C ++ 구현보다 느리지 만 너무 많지는 않습니다.

  3. scalar2.d (인덱스 / 길이 유형 변경 및 초기화되지 않은 배열 최적화) :

    allocation: 2 ms, 464 μs, and 2 hnsecs
    
    random: 5 ms, 792 μs, and 6 hnsecs
    
    result: 59
    
    scalar products: 1 sec, 859 ms, 942 μs, and 9 hnsecs

    다시 말해, ~ 1860 ms 입니다. 지금까지 이것은 주도하고 있습니다.

  4. scalar3.d (두려움) :

    allocation: 2 ms, 911 μs, and 3 hnsecs
    
    random: 7 ms, 567 μs, and 8 hnsecs
    
    result: 189
    
    scalar products: 2 secs, 182 ms, and 366 μs

    ~ 2182ms~ 보다 느리지 scalar2.d만 C ++ 버전보다 빠릅니다.

결론

올바른 최적화를 통해 D 구현은 실제로 사용 가능한 LLVM 기반 컴파일러를 사용하는 동등한 C ++ 구현보다 빠릅니다. 대부분의 응용 프로그램에서 D와 C ++ 사이의 현재 격차는 현재 구현의 한계에 기반한 것으로 보입니다.


답변

dmd는 언어의 참조 구현이므로 대부분의 작업은 백엔드를 최적화하는 대신 버그를 수정하기 위해 프론트 엔드에 배치됩니다.

“in”이 더 빠를 경우 참조 유형 인 동적 배열을 사용하게됩니다. ref를 사용하면 다른 수준의 간접 지시를 도입합니다 (일반적으로 내용뿐만 아니라 배열 자체를 변경하는 데 사용됨).

벡터는 일반적으로 const ref가 완벽하게 이해되는 구조체로 구현됩니다. 많은 벡터 연산과 임의성을 특징으로하는 실제 예제는 smallptDsmallpt 를 참조하십시오 .

64 비트도 차이를 만들 수 있습니다. x64에서 gcc는 64 비트 코드를 컴파일하는 반면 dmd는 여전히 32로 기본 설정되어 있습니다 (64 비트 codegen이 성숙 할 때 변경됨). “dmd -m64 …”로 놀라운 속도 향상이있었습니다.


답변

C ++ 또는 D가 더 빠른지 여부는 수행중인 작업에 따라 크게 달라질 수 있습니다. 잘 작성된 C ++을 잘 작성된 D 코드와 비교할 때 일반적으로 비슷한 속도이거나 C ++이 빠를 것이지만 특정 컴파일러가 최적화하도록 관리하는 것은 언어와는 별도로 큰 영향을 줄 수 있다고 생각합니다. 그 자체.

그러나이 있다 D는 속도 ++ C 박동의 좋은 기회를 의미합니다 몇 가지 경우가. 가장 중요한 것은 문자열 처리입니다. D의 배열 슬라이싱 기능 덕분에 문자열 (및 일반적으로 배열)은 C ++에서보다 훨씬 빠르게 처리 할 수 ​​있습니다. D1의 경우 Tango의 XML 프로세서는 주로 D의 어레이 슬라이싱 기능 덕분에 매우 빠릅니다 (D2는 현재 Phobos에서 작업하고있는 XML 구문 분석기가 완료되면 유사하게 빠른 XML 파서를 갖기를 바랍니다). 따라서 궁극적으로 D 또는 C ++가 더 빨라질 지 여부는 수행중인 작업에 따라 크게 달라집니다.

지금, 나는 하고 이 특정 경우에 속도가 이러한 차이를보고있는 것을 놀라게하지만 내가 DMD가 향상으로 개선 될 전망 것이라고 물건의 일종이다. gdc를 사용하면 더 나은 결과를 얻을 수 있으며 gcc 기반이기 때문에 백엔드가 아닌 언어 자체를 더 자세히 비교할 수 있습니다. 그러나 dmd가 생성하는 코드의 속도를 높이기 위해 할 수있는 일이 많이 있다면 놀라지 않을 것입니다. 나는이 시점에서 gcc가 dmd보다 성숙하다는 질문이 많이 없다고 생각합니다. 그리고 코드 최적화는 코드 성숙의 주요 결과 중 하나입니다.

궁극적으로 중요한 것은 특정 응용 프로그램에서 dmd가 얼마나 잘 수행되는지이지만 C ++과 D가 일반적으로 얼마나 잘 비교되는지 아는 것이 분명하다는 데 동의합니다. 이론적으로는 거의 동일해야하지만 실제로 구현에 달려 있습니다. 그러나 현재 두 제품이 얼마나 잘 비교되는지 테스트하려면 종합적인 벤치 마크 세트가 필요하다고 생각합니다.


답변

C 코드를 D로 작성할 수 있습니다. 빠를수록 많은 것들에 달려 있습니다.

  • 사용하는 컴파일러
  • 사용하는 기능
  • 얼마나 적극적으로 최적화

첫 번째 차이점은 불공평합니다. 두 번째 차이점은 C ++에 무거운 기능이 적기 때문에 이점이 있습니다. 세 번째는 재미있는 것입니다. 일반적으로 이해하기 쉽기 때문에 어떤 방식으로 D 코드를 최적화하는 것이 더 쉽습니다. 또한 장황하고 반복적이지만 빠른 코드와 같은 것들을 더 짧은 형태로 작성할 수 있도록 대규모 생성 프로그램을 수행 할 수 있습니다.