Pythonスクリプトが同等のC ++プログラムと同じくらい高速になるのを妨げる技術的な制限や言語機能はありますか?


10

私は長年のPythonユーザーです。数年前、私はC ++の学習を始めて、速度の点でC ++が何を提供できるかを確認しました。この間も、プロトタイピングのツールとしてPythonを使い続けました。これは良いシステムのように思われました:Pythonによるアジャイル開発、C ++での高速実行。

最近、私はますますPythonを使用しており、この言語を使用して以前にすぐに使用した落とし穴とアンチパターンをすべて回避する方法を学びました。特定の機能(リスト内包表記、列挙など)を使用するとパフォーマンスが向上することは私の理解です。

しかし、Pythonスクリプトが同等のC ++プログラムと同じくらい高速になるのを妨げる技術的な制限や言語機能はありますか?


2
はい、できます。Pythonコンパイラーの最新技術については、PyPyを参照してください。
グレッグ・ヒューギル2014

5
Pythonのすべての変数はポリモーフィックです。つまり、変数の型は実行時にのみ認識されます。Cのような言語で(整数と仮定して)x + yと表示された場合、整数の加算を行います。Pythonでは、xとyの変数タイプにスイッチがあり、適切な加算関数が選択され、オーバーフローチェックが行われてから、加算が行われます。Pythonが静的型付けを学習しない限り、このオーバーヘッドがなくなることはありません。
nwp 14

1
@nwpいいえ、それは簡単です。PyPyを参照してください。トリッキーで未解決の問題には、JITコンパイラの起動レイテンシを克服する方法、複雑な長命のオブジェクトグラフの割り当てを回避する方法、一般的にキャッシュを有効に活用する方法が含まれます。

回答:


11

数年前にフルタイムのPythonプログラミングの仕事に就いたとき、私はこの壁にちょっとぶつかりました。私はPythonが大好きですが、本当に好きですが、パフォーマンスのチューニングを始めたとき、失礼なショックがありました。

厳密なPythonistasは私を正すことができますが、ここで私が見つけたものは非常に広いストロークで描かれています。

  • Pythonのメモリ使用量はちょっと怖いです。Pythonはすべてをdictとして表現します。これは非常に強力ですが、単純なデータ型でさえ巨大になるという結果をもたらします。文字「a」が28バイトのメモリを消費したことを覚えています。Pythonでビッグデータ構造を使用している場合は、numpyまたはscipyに依存していることを確認してください。これらは直接バイト配列実装によってサポートされているためです。

これはパフォーマンスに影響を与えます。これは、他の言語と比較して、大量のメモリの周りにログを記録することに加えて、実行時に間接レベルが増えることを意味するためです。

  • Pythonにはグローバルインタープリターロックがあります。つまり、ほとんどの場合、プロセスはシングルスレッドで実行されています。プロセス間でタスクを分散するライブラリがあるかもしれませんが、Pythonスクリプトのインスタンスを32個ほど起動し、各シングルスレッドを実行していました。

他の人は実行モデルと話すことができますが、Pythonはコンパイル時にコンパイルされ、解釈されます。つまり、マシンコードに完全には行き渡りません。これもパフォーマンスに影響を与えます。CまたはC ++モジュールを簡単にリンクしたり、それらを見つけたりすることができますが、Pythonをそのまま実行すると、パフォーマンスが低下します。

現在、PythonはWebサービスのベンチマークで、RubyやPHPなどの他の実行時コンパイル言語と比較して優れています。しかし、ほとんどのコンパイル済み言語に比べてかなり遅れています。中間言語にコンパイルしてVMで実行する言語(JavaやC#など)でも、はるかに優れています。

以下は、私が時々参照する非常に興味深いベンチマークテストのセットです。

http://www.techempower.com/benchmarks/

(そうは言っても、私は今でもPythonが大好きで、使用している言語を選択する機会があれば、それが私の最初の選択です。ほとんどの場合、とにかくクレイジーなスループット要件に制約されません。)


2
文字列「a」は、最初の箇条書きの良い例ではありません。Java文字列は、単一文字の文字列に対してもかなりのオーバーヘッドがありますが、文字列の長さが長くなると、一定のオーバーヘッドで適切に償却されます(バージョン、ビルドオプション、および文字列の内容に応じて、1〜4バイトの文字)。ただし、ユーザー定義オブジェクトについては、少なくともを使用しないオブジェクトについてはそうです__slots__。PyPyはこの点ではるかに優れているはずですが、判断するのに十分な知識がありません。

1
あなたが指摘している2番目の問題は特定の実装にのみ関連しており、言語に固有のものではありません。最初の問題は説明が必要です。28バイトの「重さ」は文字自体ではなく、文字列クラスにパックされ、独自のメソッドとプロパティが付いているという事実です。単一の文字をバイト配列(リテラルb'a ')で表すと、Python 3.3で「のみ」の重みは18バイトになり、アプリケーションで本当に必要な場合は、メモリ内の文字ストレージを最適化する方法が他にもあるはずです。

C#はネイティブにコンパイルできます(たとえば、今後のMS技術、Xamarin for iOS)。
デン

13

Pythonリファレンス実装は「CPython」インタープリターです。適度に高速であるように試みますが、現在は高度な最適化を採用していません。そして、多くの使用シナリオでは、これは良いことです。いくつかの中間コードへのコンパイルはランタイムの直前に行われ、プログラムが実行されるたびにコードが新しくコンパイルされます。したがって、最適化に必要な時間は、最適化によって得られる時間と比較検討する必要があります。正味の利益がない場合、最適化は価値がありません。実行時間が非常に長いプログラム、またはループが非常にタイトなプログラムの場合、高度な最適化を使用すると便利です。ただし、CPythonは、積極的な最適化を妨げるいくつかのジョブに使用されます。

  • sysadminタスクなどに使用される短期実行スクリプト。Ubuntuのような多くのオペレーティングシステムは、Pythonの上にインフラストラクチャの優れた部分を構築しています。CPythonはこの仕事には十分高速ですが、実質的に起動時間はありません。bashよりも高速である限り、問題ありません。

  • CPythonはリファレンス実装であるため、明確なセマンティクスが必要です。これにより、「foo演算子の実装を最適化する」や「リスト内包をより高速なバイトコードにコンパイルする」などの単純な最適化が可能になりますが、一般に、関数のインライン化などの情報を破壊する最適化はできません。

もちろん、CPython以外にも多くのPython実装があります。

  • JythonはJVMの上に構築されています。JVMは、提供されたバイトコードを解釈またはJITコンパイルでき、プロファイルに基づく最適化機能を備えています。起動時間が長く、JITが始まるまでに時間がかかります。

  • PyPyは最先端のJITting Python VMです。PyPyは、Pythonの制限されたサブセットであるRPythonで記述されています。このサブセットは、Pythonから表現力をある程度取り除きますが、任意の変数の型を静的に推論することができます。次に、RPythonで作成されたVMをCにトランスパイルして、RPython Cのようなパフォーマンスを実現できます。ただし、RPythonはCよりも表現力が高く、新しい最適化をより迅速に開発できます。PyPyはコンパイラのブートストラップの例です。PyPy(RPythonではありません!)は、CPythonリファレンス実装とほぼ互換性があります。

  • Cythonは(RPythonのように)静的型付けが可能な非互換のPython方言です。また、Cコードに変換され、CPythonインタープリター用のC拡張を簡単に生成できます。

PythonコードをCythonまたはRPythonに変換したい場合は、Cのようなパフォーマンスが得られます。ただし、「Pythonのサブセット」としてではなく、「Python構文を使用したC」として理解してください。PyPyに切り替えると、通常のPythonコードは大幅に速度が向上しますが、CまたはC ++で記述された拡張機能とインターフェースすることもできなくなります。

しかし、起動時間が長いことを除いて、バニラPythonがCのようなパフォーマンスレベルに到達するのを妨げるプロパティや機能は何ですか?

  • 寄稿者と資金。JavaやC#とは異なり、この言語をそのクラスで最高にすることに関心を持つ言語の背後に単一の推進会社はありません。これにより、開発は主にボランティアに、そして時折の助成に限定されます。

  • 遅延バインディングと静的型付けの欠如。Pythonでは、次のようながらくたを書くことができます。

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding
    

    Pythonでは、任意の変数をいつでも再割り当てできます。これにより、キャッシュやインライン化が防止されます。すべてのアクセスは変数を通過する必要があります。この間接性により、パフォーマンスが低下します。もちろん、コードがそのようなめちゃくちゃなことをしないので、コンパイル前に各変数に明確な型を指定でき、各変数が1回だけ割り当てられる場合、理論的には、より効率的な実行モデルを選択できます。これを念頭に置いた言語は、識別子を定数としてマークする方法を提供し、少なくともオプションの型注釈(「段階的型付け」)を許可します。

  • 問題のあるオブジェクトモデル。スロットが使用されない限り、オブジェクトがどのフィールドを持っているかを理解するのは困難です(Pythonオブジェクトは基本的にフィールドのハッシュテーブルです)。そして、いったんそこに来たとしても、これらのフィールドがどんな型を持っているのかはまだわかりません。これにより、C ++の場合のように、オブジェクトを密にパックされた構造体として表すことができなくなります。(もちろん、C ++によるオブジェクトの表現も理想的ではありません。構造体のような性質のため、プライベートフィールドでさえオブジェクトのパブリックインターフェイスに属しています。)

  • ガベージコレクション。多くの場合、GCは完全に回避できます。C ++では、現在のスコープを離れると自動的に破棄されるオブジェクトを静的に割り当てることができますType instance(args);。それまでは、オブジェクトは生きており、他の機能に貸すことができます。これは通常、「参照渡し」を介して行われます。Rustのような言語では、そのようなオブジェクトへのポインターがオブジェクトの存続期間を超えていないことをコンパイラーが静的にチェックできます。このメモリ管理スキームは完全に予測可能で、非常に効率的で、複雑なオブジェクトグラフがなくてもほとんどの場合に適しています。残念ながら、Pythonはメモリ管理を考慮して設計されていません。理論的には、エスケープ分析を使用して、GCを回避できるケースを見つけることができます。実際には、次のような単純なメソッドチェーンfoo().bar().baz() ヒープに多数の短期間のオブジェクトを割り当てる必要があります(世代別GCは、この問題を小さく保つ1つの方法です)。

    他の場合では、プログラマーはリストなどのオブジェクトの最終的なサイズをすでに知っている場合があります。残念ながら、Pythonは新しいリストを作成するときにこれを通信する方法を提供していません。代わりに、新しいアイテムが最後にプッシュされ、複数の再割り当てが必要になる場合があります。いくつかのメモ:

    • 特定のサイズのリストはのように作成できますfixed_size = [None] * size。ただし、そのリスト内のオブジェクトのメモリは、個別に割り当てる必要があります。対照的なC ++ std::array<Type, size> fixed_size

    • 特定のネイティブタイプのパックされた配列は、array組み込みモジュールを介してPythonで作成できます。また、numpyネイティブの数値型に対して特定の形状を持つデータバッファーの効率的な表現を提供します。

概要

Pythonは、パフォーマンスではなく使いやすさを考慮して設計されています。その設計により、非常に効率的な実装を作成することがかなり難しくなっています。プログラマーが問題のある機能を放棄した場合、残りのイディオムを理解しているコンパイラーは、Cのパフォーマンスに匹敵する効率的なコードを生成できます。


8

はい。主な問題は、言語が動的であると定義されていることです。つまり、実行しようとするまで、何をしているのかを知ることができません。あなたがマシンコードを生成するかわからないので、それは、それは非常に難しい効率的なマシンコードを生成することを可能にするために。JITコンパイラはこの領域でいくつかの作業を行うことができますが、JITコンパイラはプログラムの実行に費やしていない時間とメモリであり、実行に時間とメモリを費やすことができないため、C ++に匹敵することはありません。動的言語のセマンティクスを壊すことなく達成できます。

これが容認できないトレードオフであると主張するつもりはありません。しかし、実際の実装がC ++実装ほど高速になることはありません。


8

すべての動的言語のパフォーマンスに影響を与える主な要因は3つあります。

  1. 解釈上のオーバーヘッド。実行時には、機械命令ではなく、ある種のバイトコードがあり、このコードを実行するための固定オーバーヘッドがあります。
  2. オーバーヘッドをディスパッチします。関数呼び出しのターゲットは実行時までわからないため、呼び出すメソッドを見つけるとコストがかかります。
  3. メモリ管理オーバーヘッド。動的言語は、割り当てと割り当て解除が必要なオブジェクトにデータを格納し、パフォーマンスのオーバーヘッドをもたらします。

C / C ++の場合、これら3つの要素の相対コストはほぼゼロです。命令はプロセッサによって直接実行され、ディスパッチは最大で1つまたは2つの間接参照を使用します。特に断らない限り、ヒープメモリは割り当てられません。よく書かれたコードはアセンブリ言語に近づくかもしれません。

JITコンパイルを使用したC#/ Javaの場合、最初の2つは少ないですが、ガベージコレクションされたメモリにはコストがかかります。よく書かれたコードは2x C / C ++に近づくかもしれません。

Python / Ruby / Perlの場合、これら3つの要素すべてのコストは比較的高くなります。C / C ++と比較して5倍かそれ以下と考えてください。(*)

ランタイムライブラリのコードは、プログラムと同じ言語で記述されている場合があり、同じパフォーマンス制限があることに注意してください。


(*)Just-In_Time(JIT)コンパイルがこれらの言語に拡張されているため、適切に記述されたC / C ++コードの速度に(通常2倍)近づきます。

また、(競合する言語間の)ギャップが狭くなると、違いはアルゴリズムと実装の詳細によって決まります。JITコードはC / C ++に勝る場合があり、C / C ++はアセンブリ言語に勝る場合があります。優れたコードを記述する方が簡単だからです。


「ランタイムライブラリのコードは、プログラムと同じ言語で記述されている場合があり、パフォーマンスの制限も同じであることを忘れないでください。」「Python / Ruby / Perlの場合、これら3つの要素すべてのコストは比較的高くなります。C/ C ++と比較して5倍かそれ以下と考えてください。」実際、そうではありません。たとえば、Rubinius Hashクラス(Rubyのコアデータ構造の1つ)はRubyでHash記述されており、Cで記述されたYARVのクラスよりも、場合によってはさらに高速で実行されます。そして、Rubiniusのランタイムの大部分は、システムは、その彼らができること、Rubyで書かれている...
イェルクWミッターク

…例えば、Rubiniusコンパイラーによってインライン化されます。極端な例として、Klein VM(SelfのメタサーキュラーVM)とMaxine VM(JavaのメタサーキュラーVM)があります。ここでは、メソッドディスパッチコード、ガベージコレクター、メモリアロケーター、プリミティブ型、コアデータ構造、およびアルゴリズムもすべて記述されています。自己またはJava。このようにして、コアVMの一部でもユーザーコードにインライン化でき、VMはユーザープログラムからのランタイムフィードバックを使用して再コンパイルおよび再最適化できます。
イェルクWミッターク

@JörgWMittag:まだ本当です。RubiniusにはJITがあり、JITコードは多くの場合、個々のベンチマークでC / C ++に勝っています。JITが存在しない場合、このメタサーキュラーが速度を向上させるという証拠はありません。[JITについては、編集を参照してください。]
david.pfx 14

1

しかし、Pythonスクリプトが同等のC ++プログラムと同じくらい高速になるのを妨げる技術的な制限や言語機能はありますか?

いいえ。C++を高速に実行するために注がれるお金とリソースと、Pythonを高速に実行するために注がれるお金とリソースの問題です。

たとえば、Self VMが登場したとき、それは最速の動的OO言語であっただけでなく、最速のOO言語期間でもありました。信じられないほど動的な言語であるにもかかわらず(たとえば、Python、Ruby、PHP、JavaScriptよりはるかに)、利用可能なほとんどのC ++実装よりも高速でした。

しかし、SunはSelfプロジェクト(大規模システムを開発するための成熟した汎用OO言語)をキャンセルし、テレビセットトップボックスのアニメーションメニュー用の小さなスクリプト言語に焦点を当てました(聞いたことがあるかもしれませんが、Javaと呼ばれています)。より多くの資金。同時に、Intel、IBM、Microsoft、Sun、Metrowerks、HPなど。莫大な金額とリソースを費やしてC ++を高速化しました。CPUメーカーは、C ++を高速化するためにチップに機能を追加しました。オペレーティングシステムは、C ++を高速にするために作成または変更されました。したがって、C ++は高速です。

私はPythonにそれほど精通していないので、私はRubyの人間なので、Rubyの例を挙げHashますdict。RubiniusRuby実装のクラス(Python での機能と重要性は同等)は、100%純粋なRubyで記述されています。しかし、それは有利に競争し、時にはHash手作業で最適化されたCで書かれたYARV のクラスよりも優れています。そして、商用のLispまたはSmalltalkシステム(または前述のSelf VM)と比較すると、Rubiniusのコンパイラーはそれほど賢くありません。

Pythonには、処理速度を低下させる固有の要素はありません。今日のプロセッサとオペレーティングシステムには、Pythonに悪影響を与える機能があります(たとえば、仮想メモリはガベージコレクションのパフォーマンスが悪いことが知られています)。C ++は役立つがPythonは役に立たない機能があります(最新のCPUは非常に高価であるため、キャッシュミスを回避しようとします。残念ながら、OOとポリモーフィズムがある場合、キャッシュミスを回避することは困難です。むしろ、キャッシュのコストを削減する必要があります。 Java用に設計されたAzul Vega CPUがこれを行います。)

C ++の場合と同様に、Pythonを高速化するために多くのお金、研究、およびリソースを費やし、C ++の場合と同様に、Pythonプログラムを高速に実行するオペレーティングシステムを作るために多くのお金、研究、およびリソースを費やす場合PythonプログラムをC ++の場合と同じように高速に実行するCPUを作成するための多くのお金、研究、リソースがあれば、PythonがC ++に匹敵するパフォーマンスに到達できることは間違いありません。

ECMAScriptでは、1人のプレーヤーだけがパフォーマンスに真剣に取り組むとどうなるかを見てきました。1年以内に、基本的にすべての主要ベンダーのパフォーマンスが全体で10倍に向上しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.