web-dev-qa-db-ja.com

scalaコードに対して何ができるので、より速くコンパイルできますか?

scalaコードベースがあります。( https://opensource.ncsa.illinois.edu/confluence/display/DFDL/Daffodil%3A+Open+Source+DFDL

scalaコードの70K行。我々はscala 2.11.7

コンパイル-編集-コンパイル-テスト-デバッグのサイクルは小さな変更には長すぎるため、開発は難しくなっています。

増分再コンパイル時間は1分になる場合があり、これは最適化がオンになっていない場合です。時々長くなります。そして、それはファイルへの非常に多くの変更を編集していないということです。場合によっては、非常に小さな変更でも大きな再コンパイルが発生することがあります。

だから私の質問:コードを整理することで何ができますか、それはコンパイル時間を改善しますか?

たとえば、コードを小さなファイルに分解しますか?これは役立ちますか?

たとえば、より小さなライブラリですか?

たとえば、暗黙の使用を回避しますか? (非常に少ない)

たとえば、特性の使用を回避しますか? (トンあります)

たとえば、大量の輸入を避けますか? (私たちはたくさんあります-パッケージの境界はこの時点ではかなり混oticとしています)

それとも、これに関して私ができることは本当に何もないのですか?

この非常に長いコンパイルは、依存関係による再コンパイルの膨大な量に何らかの原因があるように感じ、false依存関係を減らす方法を考えています。 ..しかし、それは単なる理論です

他の誰かが少しずつ光を当てることを期待していますsomethingこれにより、インクリメンタルな変更のコンパイル速度が向上します。

43
Mike Beckerle

scalaコンパイラーのフェーズと、ソースコードからのコメントのわずかに編集されたバージョンを示します。このコンパイラーは、型チェックとより多くの変換に重点が置かれる点で異常であることに注意してください他のコンパイラには、最適化、レジスタ割り当て、IRへの変換のための多くのコードが含まれています。

いくつかのトップレベルのポイント:多くのツリーの書き換えがあります。各フェーズは、前のフェーズからツリーを読み取り、それを新しいツリーに変換する傾向があります。対照的に、シンボルはコンパイラーの存続期間を通じて意味を持ちます。したがって、ツリーはシンボルへのポインタを保持しますが、逆も同様です。シンボルを書き換える代わりに、フェーズが進むにつれて新しい情報がシンボルに付加されます。

グローバルのフェーズのリスト:

 analyzer.namerFactory: SubComponent,
    analyzer.typerFactory: SubComponent,
    superAccessors,  // add super accessors
    pickler,         // serializes symbol tables
    refchecks,       // perform reference and override checking,
translate nested objects
    liftcode,        // generate reified trees
    uncurry,         // uncurry, translate function values to anonymous
classes
    tailCalls,       // replace tail calls by jumps
    explicitOuter,   // replace C.this by explicit outer pointers,
eliminate pattern matching
    erasure,         // erase generic types to Java 1.4 types, add
interfaces for traits
    lambdaLift,      // move nested functions to top level
    constructors,    // move field definitions into constructors
    flatten,         // get rid of inner classes
    mixer,           // do mixin composition
    cleanup,         // some platform-specific cleanups
    genicode,        // generate portable intermediate code
    inliner,         // optimization: do inlining
    inlineExceptionHandlers, // optimization: inline exception handlers
    closureElimination, // optimization: get rid of uncalled closures
    deadCode,           // optimization: get rid of dead cpde
    if (forMSIL) genMSIL else genJVM, // generate .class files

scalaコンパイラー の回避策

したがって、scalaコンパイラはJavaコンパイラよりも多くの作業を行う必要がありますが、特にScalaコンパイラは大幅に遅くなります。

  • 暗黙的な解像度。暗黙的な解決(つまり、暗黙の宣言を行うときに暗黙の値を見つけようとするscalac)は、宣言内のすべての親スコープにバブルアップします。この検索時間は膨大です(特に、同じ暗黙の変数を何度も参照する場合、依存関係チェーンのずっと下のいくつかのライブラリで宣言されています)。暗黙の特性解決と型クラスを考慮すると、コンパイル時間はさらに悪化します。これは、scalazやshapelessなどのライブラリで頻繁に使用されます。また、膨大な数の匿名クラス(ラムダ、ブロック、匿名関数)を使用します。マクロは明らかにコンパイル時間に追加されます。

    Martin Oderskyによる非常に素晴らしい記事

    さらにJavaおよびScalaコンパイラはソースコードをJVMバイトコードに変換し、最適化をほとんど行いません。ほとんどの最新のJVMでは、プログラムのバイトコードが実行されると、実行されているコンピュータアーキテクチャのマシンコードに変換されます。これはジャストインタイムコンパイルと呼ばれますが、ジャストインタイムコンパイルでは高速である必要があるため、コード最適化のレベルは低くなります。再コンパイルを回避するために、いわゆるHotSpotコンパイラは、頻繁に実行されるコードの部分のみを最適化します。

    プログラムは、実行されるたびにパフォーマンスが異なる場合があります。同じJVMインスタンスで同じコード(メソッドなど)を複数回実行すると、実行の間に特定のコードが最適化されているかどうかによってパフォーマンスが大きく異なる場合があります。さらに、コードの一部の実行時間を測定すると、JITコンパイラ自体が最適化を実行していた時間が含まれるため、一貫性のない結果が得られます。

    パフォーマンス低下の一般的な原因の1つは、プリミティブ型を引数としてジェネリックメソッドと頻繁なGCに渡すときに暗黙的に行われるボックス化とボックス化解除です。

    上記の影響を測定中に回避するには、より積極的な最適化を行うサーバーバージョンのHotSpot JVMを使用して実行する必要があります。Visualvmは、JVMアプリケーションのプロファイリングに最適です。これは、いくつかのコマンドラインJDKツールと軽量プロファイリング機能を統合したビジュアルツールです。ただし、scala abstracionsは非常に複雑であり、残念ながらVisualVMは、 Scalaコレクションのメソッドである多くのexistsおよびforallを使用します。これらのコレクションは、述語、述語をFOLに渡し、シーケンス全体を最大化してパフォーマンスを最大化できます。

    また、モジュールを協調的で依存性の少ないものにすることも実行可能なソリューションです。中間コードの生成は、時々マシンに依存し、さまざまなアーキテクチャはさまざまな結果をもたらします。

    代替案:Typesafeは、高速インクリメンタルコンパイラをsbtから分離し、maven /他のビルドツールがそれを使用できるようにするZincをリリースしました。したがって、scala mavenプラグインでZincを使用すると、コンパイルが非常に高速になります。

    簡単な問題:整数のリストが与えられたら、最大のものを削除します。注文は必要ありません。

以下は、ソリューションのバージョンです(私が推測する平均)。

def removeMaxCool(xs: List[Int]) = {
  val maxIndex = xs.indexOf(xs.max);
  xs.take(maxIndex) ::: xs.drop(maxIndex+1)
}

Scala慣用的で簡潔で、いくつかのNiceリスト関数を使用します。非常に非効率的です。少なくとも3回または4回リストを走査します。

ここで、Javaに似たソリューションを検討してください。また、合理的なJava開発者(またはScala初心者)が書くものでもあります。

def removeMaxFast(xs: List[Int]) = {
    var res = ArrayBuffer[Int]()
    var max = xs.head
    var first = true;   
    for (x <- xs) {
        if (first) {
            first = false;
        } else {
            if (x > max) {
                res.append(max)
                max = x
            } else {
                res.append(x)
            }
        }
    }
    res.toList
}

完全に非Scalaの慣用的、非機能的、非簡潔ですが、非常に効率的です。それは一度だけリストをたどります!

そのため、トレードオフにも優先順位を付ける必要があり、場合によっては、Java開発者が他にいない場合は開発者のようなことをする必要があります。

6
khakishoiab

役立つかもしれないいくつかのアイデア-あなたのケースと開発スタイルに依存します:

  • インクリメンタルコンパイルを使用する~compile SBTまたはIDEによって提供されます。
  • sbt-revolver を使用し、JRebelを使用してアプリをより速くリロードします。 Webアプリに適しています。
  • アプリ全体のテストを実行してデバッグするのではなく、TDDを使用して、テストのみを実行します。
  • プロジェクトをライブラリ/ JARに分割します。これらをビルドツールSBT/Maven/etcを介して依存関係として使用します。またはこの次のバリエーション...
  • プロジェクトをサブプロジェクト(SBT)に分割します。必要なものを別にコンパイルするか、すべてが必要な場合はルートプロジェクトをコンパイルします。インクリメンタルコンパイルは引き続き使用可能です。
  • プロジェクトをマイクロサービスに分類します。
  • Dottyが問題をある程度解決するまで待ちます。
  • すべてが失敗した場合、高度なScalaコンパイルを遅くする機能:暗黙的、メタプログラミングなど)を使用しないでください。
  • Scalaコンパイラに十分なメモリとCPUを割り当てていることを忘れないでください。私は試したことはありませんが、おそらくRAMソースおよびコンパイルアーティファクトにHDDの代わりにディスク(Linuxで簡単)。
4
yǝsʞǝla

私は、オブジェクト指向設計の主な問題の1つに触れています(オーバーエンジニアリング)。クラスオブジェクトの特性の階層を平坦化し、クラス間の依存関係を減らす必要があると思います。パッケージを異なるjarファイルにブレーキし、それらを「凍結」された新しいライブラリに集中するミニライブラリとして使用します。

OO over-engineeringに反対するケースを作っているBrian Willのビデオもチェックしてください。

つまり https://www.youtube.com/watch?v=IRTfhkiAqPw (良い点が取れる)

私は彼に100%同意しませんが、それはオーバーエンジニアリングに対して良いケースになります。

お役に立てば幸いです。

1
firephil

Fast Scala Compiler を使用してみてください。

0
jlncrnt

(例えば@tailrec注釈)、あなたがどれだけ勇敢であるかに応じて、 Dotty をいじることもできます。

0
airudah