ホームページ  >  記事  >  バックエンド開発  >  99%の人は知らない! Python、C、C拡張、Cythonの違い比較!

99%の人は知らない! Python、C、C拡張、Cythonの違い比較!

WBOY
WBOY転載
2023-04-14 17:40:031963ブラウズ

99%の人は知らない! Python、C、C拡張、Cythonの違い比較!

シンプルなフィボナッチ数列を例として、実行効率の違いをテストしてみましょう。

Python コード:

def fib(n):
a, b = 0.0, 1.0
for i in range(n):
a, b = a + b, a
return a

C コード:

double cfib(int n) {
int i;
double a=0.0, b=1.0, tmp;
for (i=0; i<n; ++i) {
tmp = a; a = a + b; b = tmp;
}
return a;
}

上記は、C で実装されたフィボナッチ数列です。なぜ整数ではなく浮動小数点型を使用するのか不思議に思う人もいるかもしれません。答えは、C の整数型には範囲があるので double を使用し、Python の float は最下層の PyFloatObject に相当し、これも内部的に double で格納されます。

C 拡張機能:

C 拡張機能、注: C 拡張機能はここでは焦点ではありません。C 拡張機能を記述します。 Cython を書くことの本質は、Python の拡張モジュールを書くことと同じですが、Cython を書くことは、C 拡張機能を書くことよりもはるかに簡単であることは間違いありません。

#include "Python.h"

double cfib(int n) {
int i;
double a=0.0, b=1.0, tmp;
for (i=0; i<n; ++i) {
tmp = a; a = a + b; b = tmp;
}
return a;
}

static PyObject *fib(PyObject *self, PyObject *n) {
if (!PyLong_CheckExact(n)) {
wchar_t *error = L"函数 fib 需要接收一个整数";
PyErr_SetObject(PyExc_ValueError,
PyUnicode_FromWideChar(error, wcslen(error)));
return NULL;
}
double result = cfib(PyLong_AsLong(n));
return PyFloat_FromDouble(result);
}

static PyMethodDef methods[] = {
{"fib",
 (PyCFunction) fib,
 METH_O,
 "这是 fib 函数"},
 {NULL, NULL, 0, NULL}
};

static PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"c_extension",
"这是模块 c_extension",
-1,
methods,
NULL, NULL, NULL, NULL
};

PyMODINIT_FUNC PyInit_c_extension(void) {
return PyModule_Create(&module);
}

C 拡張機能を作成すると、単純なフィボナッチであっても非常に複雑であることがわかります。

Cython コード:

最後に、Cython を使用してフィボナッチを記述する方法を見てみましょう。 Cython で書かれたコードはどのようになるべきだと思いますか?

def fib(int n):
cdef int i
cdef double a = 0.0, b = 1.0
for i in range(n):
a, b = a + b, a
return a

どうですか、Cython コードと Python コードはよく似ていますか?まだ Cython の構文を正式に学習していませんが、上記のコードが何を意味するか推測できるはずです。 cdef キーワードを使用して C レベル変数を定義し、その型を宣言しました。

Cython コードは、インタプリタによって認識される前に拡張モジュールにコンパイルする必要があるため、まず C コードに変換してから、拡張モジュールにコンパイルする必要があります。繰り返しますが、C 拡張機能の作成と Cython の作成には本質的な違いはなく、Cython コードも C コードに変換する必要があります。

しかし、Cython を書く方が C 拡張機能を書くよりもはるかに簡単であることは明らかです。書かれた Cython コードの品質が高ければ、翻訳された C コードの品質も高くなるでしょう。 、および翻訳 このプロセスでは、最大限の最適化も自動的に実行されます。しかし、それが手書きの C 拡張機能の場合、開発者はすべての最適化を手動で処理する必要があり、関数が複雑な場合は C 拡張機能を記述すること自体が頭の痛い問題であることは言うまでもありません。

Cython はなぜ高速化できるのでしょうか?

Cython コードを純粋な Python フィボナッチと比較すると、変数 i、a、b の型が事前に指定されていることが違いであることがわかります。重要なのは、なぜこれに加速効果があるのか​​ということです(まだテストされていませんが、間違いなく速度が上がります。そうでない場合は、Cythonを学ぶ必要はありません)。

しかし、その理由はここにあります。Python のすべての変数は汎用ポインター PyObject * であるからです。 PyObject (C の構造体) には 2 つの内部メンバーがあります。ob_refcnt: オブジェクトの参照カウントを保持します。ob_type *: オブジェクト タイプのポインターを保持します。

整数、浮動小数点数、文字列、タプル、辞書など、それらを指すすべての変数は PyObject * です。運用する場合は、-> ob_type で対応する型のポインタを取得し、変換する必要があります。

たとえば、Python コードの a と b の場合、どのレベルのループが実行されても、結果は浮動小数点数を指すことがわかっていますが、インタープリタは浮動小数点数を指しません。この推論をします。各加算を検出して、その型を判断して変換する必要があります。その後、加算を実行するときに、内部 __add__ メソッドに移動して 2 つのオブジェクトを追加し、新しいオブジェクトを作成します。実行が完了した後、ポインタをこの新しいオブジェクトに変換します。 PyObject * に変換して戻ります。

そして、Python オブジェクトはヒープ上にスペースを割り当て、a と b は不変であるため、サイクルごとに新しいオブジェクトが作成され、前のオブジェクトがリサイクルされます。

上記のすべての結果、Python コードの実行効率を高くすることは不可能になりました。Python にはメモリ プールと対応するキャッシュ メカニズムも提供されていますが、明らかにまだ耐えられません。効率が低いこと。

Cython が高速化できる理由については、後ほど説明します。

効率の違い

では、両者の効率の違いは何でしょうか?表を使用して比較してみましょう:

99%の人は知らない! Python、C、C拡張、Cythonの違い比較!

改善倍数とは、純粋な Python と比較して効率が何倍向上したかを指します。

2 番目の列は fib(0) です。明らかに、実際にはループに入りません。fib(0) は、関数呼び出しのコストを測定します。最後から 2 番目の列「ループ本体の消費時間」は、関数呼び出し自体のオーバーヘッドを除いた、fib(90) の実行時に内側のループ本体の実行に費やされた時間を指します。

全体として、純粋な C 言語で書かれたフィボナッチが最も速いのは間違いありませんが、考慮すべき点がたくさんあります。

Pure Python

予想どおり、あらゆる面でパフォーマンスが最悪です1つ。 fib(0) から判断すると、関数の呼び出しには 590 ナノ秒かかり、C よりもはるかに遅いです。その理由は、Python は関数呼び出し時にスタック フレームを作成する必要があり、このスタック フレームはヒープ上に割り当てられるためです。最後には、スタック フレームなどの破壊も伴います。 fib(90) に関しては、明らかに分析は必要ありません。

純粋な C

明らかに、現時点では Python ランタイムとの対話がないため、パフォーマンスの消費は最小限です。 。 fib(0) は、C での関数の呼び出しに 2 ナノ秒しかかからないことを示し、fib(90) は、ループの実行が C の方が Python よりもほぼ 80 倍高速であることを示しています。

C 拡張機能

#C 拡張機能が行うことはすでに上で説明したように、C を使用して拡張機能を記述することです。 Python の場合はモジュール。ループ本体の消費時間を確認すると、C 拡張機能は純粋な C とほぼ同じであることがわかります。違いは、関数呼び出しにより多くの時間が費やされることです。その理由は、拡張モジュールの関数を呼び出すときに、まず Python データを C データに変換し、次に C 関数を使用してフィボナッチ数列を計算し、その後 C データを Python データに変換する必要があるためです。

つまり、C 拡張機能は本質的には C 言語ですが、C コードを pyd ファイルにコンパイルして直接実行できるように、作成時に CPython によって提供される API 仕様に従う必要があります。 Python do it.転送。結果の観点からは、Cython と同じです。ただし、繰り返しになりますが、C で拡張機能を作成することは、本質的に C を作成することと同じであり、基盤となる Python/C API にも精通している必要がありますが、これは比較的難しいです。

Cython

ループ本体時間だけを見ると、純粋な C、C 拡張、Cython は次のようになります。すべてほぼ同じですが、Cython で記述するのが明らかに最も便利です。 Cython の機能は本質的に C 拡張機能に似ていると言われていますが、どちらも Python 用の拡張モジュールを提供しています。違いは、1 つは手動で C コードを記述すること、もう 1 つは Cython コードを記述してから自動的に C コードに変換することです。したがって、Cython では、Python データを C データに変換し、計算を実行し、Python データを元に戻すというプロセスが避けられません。

しかし、Cython は C 拡張機能よりも関数呼び出しにかかる時間がはるかに短いことがわかります。その主な理由は、Cython によって生成された C コードが高度に最適化されているためです。しかし、正直なところ、関数呼び出しにかかる時間はそれほど気にする必要はなく、注目すべきは内部コードブロックの実行にかかる時間です。もちろん、関数呼び出し自体のオーバーヘッドを削減する方法についても後で説明します。

Python の for ループはなぜこんなに遅いのでしょうか?

ループ本体の時間消費量から、Python の for ループが非常に遅いことで悪名高いことがわかります。その理由は何でしょうか?それを分析してみましょう。

1. Python の for ループ メカニズム

Python が反復可能オブジェクトを走査するとき、最初に __iter__ メソッドを呼び出します。 iterable オブジェクト内では、対応するイテレータが返されます。その後、イテレータが StopIteration 例外をスローするまで、継続的にイテレータの __next__ メソッドを呼び出して値を 1 つずつ繰り返します。StopIteration 例外は for ループによってキャプチャされ、ループが終了します。

イテレータはステートフルであり、Python インタープリタはイテレータの反復ステータスを常に記録する必要があります。

2. Python での算術演算

これについては実際に上で説明しましたが、Python 自体の動的な特性により、Python は型ベースの最適化を行うことができません。

例: ループ本体内の a b、この a と b は、整数、浮動小数点数、文字列、タプル、リスト、またはマジック メソッド __add__ Instance を実装したものを指すこともできます。クラスのオブジェクトなど。

これが浮動小数点数であることはわかっていますが、Python ではこれを想定していないため、 a b が実行されるたびに、その型は何でしょうか?次に、内部に __add__ メソッドがあるかどうかを確認し、存在する場合は、パラメータとして a と b を使用して 呼び出しを実行し、a と b が指すオブジェクトを追加します。結果が計算された後、そのポインターは PyObject * に変換されて返されます。

C と Cython では、変数を作成するときに、型があらかじめ double で指定されており、それ以外の型は指定されないため、コンパイルされた a b は単なる機械命令にすぎません。それに比べて、Python が遅くないはずがありません。

3. Python オブジェクトのメモリ割り当て

Python オブジェクトはヒープ上に割り当てられるため、Python オブジェクトは本質的に次のとおりです。 C の malloc 関数は、構造体のヒープ領域内のメモリの一部に適用されます。ヒープ領域へのメモリの割り当てと解放には多大なコストがかかりますが、スタックははるかに小さく、オペレーティング システムによって維持され、自動的にリサイクルされるため、非常に効率的です。必要なのは 1 回の移動です。登録するだけです。

しかし、明らかにヒープにはこの処理はなく、Python オブジェクトはすべてヒープ上に割り当てられます。Python ではメモリ プール メカニズムが導入されていますが、オペレーティング システムとの頻繁なやり取りが回避されます。インタラクションのほか、小さな整数オブジェクト プール、文字列インターン メカニズム、キャッシュ プールなども導入されています。

しかし実際には、オブジェクト (スカラーを含むあらゆるオブジェクト) の作成と破棄に関しては、動的に割り当てられるメモリと Python のメモリ サブシステムのオーバーヘッドが増加します。 float オブジェクトは不変であるため、ループするたびに作成および破棄されるため、効率はまだ高くありません。

そして、Cython によって割り当てられた変数 (型が C の型の場合) は、現在の a と b に対してポインターではなくなります (Python 変数はポインターです)。 、スタック上に割り当てられた倍精度浮動小数点数です。スタック上での割り当て効率はヒープよりもはるかに高いため、for ループに非常に適しており、効率は Python よりもはるかに高くなります。また、割り当てだけでなく、アドレス指定の際もヒープよりもスタックの方が効率的です。

したがって、Python は反復ごとに多くの作業を行うため、for ループに関しては C と Cython が純粋な Python よりも桁違いに高速であることは驚くべきことではありません。

Cython を使用するのはどのような場合ですか?

Cython コードでは、いくつかの cdef を追加するだけで、これほど大幅なパフォーマンスの向上が達成できることがわかり、これは明らかに非常に興味深いことです。ただし、Cython で記述した場合、すべての Python コードでパフォーマンスが大幅に向上するわけではありません。

ここでのフィボナッチ数列の例は、内部のデータが CPU にバインドされており、ランタイムが CPU レジスタ内のいくつかの変数の処理に費やされるため、意図的なものであり、データの移動は必要ありません。この関数が次の作業を行う場合:

  • 大きな配列への要素の追加など、メモリを大量に消費する;
  • I/Oディスクからの大きなファイルの読み取りなど、集中的です。
  • FTP サーバーからのファイルのダウンロードなど、ネットワークに集中的です。

##その後 Python、C、および Cython の違いは、(ストレージ集約型の操作の場合) 大幅に減少するか、(I/O 集約型またはネットワーク集約型の操作の場合) 完全になくなる場合もあります。

Python プログラムのパフォーマンスの向上が目標である場合、パレートの法則が非常に役立ちます。つまり、プログラムの実行時間の 80% は原因で構成されています。コード20で。しかし、注意深く分析しないと、これらの 20% のコードを見つけるのは困難です。したがって、Cython を使用してパフォーマンスを向上させる前に、ビジネス ロジック全体を分析することが最初のステップとなります。

分析を通じて、プログラムのボトルネックがネットワーク IO によって引き起こされていると判明した場合、Cython が大幅なパフォーマンスの向上をもたらすことは期待できません。したがって、Cython を使用する前に、まずプログラムのボトルネックの原因を特定する必要があります。したがって、Cython は強力なツールですが、正しい方法で使用する必要があります。

さらに、Cython は Python に C 型システムを導入したため、C データ型の制限に注意する必要があります。 Python の整数には長さの制限がないことはわかっていますが、C の整数には制限があるため、無限精度の整数を正しく表現できないことになります。

ただし、Cython のいくつかの機能は、これらのオーバーフローを捕捉するのに役立ちます。つまり、最も重要なことは、C データ型は Python データ型より高速ですが、制限があるということです。そのため、柔軟性や汎用性が低下します。ここから、Python が速度、柔軟性、汎用性の観点から後者を選択したこともわかります。

Cython のもう 1 つの機能である外部コードへの接続についても考慮してください。出発点が Python ではなく C または C であり、Python を使用して複数の C または C モジュールを接続したいとします。 Cython は C および C 宣言を理解し、高度に最適化されたコードを生成できるため、ブリッジとしてより適しています。

私は Python 専攻なので、C と C が関係する場合、Cython に C と C を導入し、すでに書かれている C ライブラリを直接呼び出す方法を紹介します。複数の C および C モジュールを接続するブリッジとして Cython を C および C に導入する方法は紹介しません。これを理解していただければ幸いです。私はサービスを作成するために C や C を使用するのではなく、Python の効率を向上させるためにのみサービスを使用します。

概要

これまで、Cython の紹介のみを行い、主にその位置付けと Python および C との関係について説明しました。 Cythonを使ってPythonを高速化する方法やCythonコードの書き方、詳しい構文については後ほど紹介します。

つまり、Cython は Python に対応する成熟した言語です。 Cython コードは Python の構文規則に準拠していないため、直接実行できません。

Cython の使用方法は次のとおりです。まず Cython コードを C コードに変換し、次に C コードを拡張モジュール (pyd ファイル) にコンパイルして、それを Python コードにインポートします。内部の関数メソッドを呼び出すのは正しい方法であり、もちろん Cython を使用する唯一の方法です。

たとえば、上記の Cython で作成したフィボナッチは、直接実行するとエラーを報告します。これは、cdef が明らかに Python の構文ルールに準拠していないためです。したがって、Cython コードを拡張モジュールにコンパイルし、通常の py ファイルにインポートする必要がありますが、これは実行速度を向上させるために重要です。したがって、Cython コードは CPU を大量に使用するコードである必要があります。そうしないと、効率を大幅に向上させることが困難になります。

そのため、Cython を使用する前に、ビジネス ロジックを注意深く分析するか、当面は Cython を使用せずに完全に Python で記述することをお勧めします。書き込みが完了したら、プログラムのパフォーマンスのテストと分析を開始して、時間がかかる部分を確認しますが、同時に静的型付けによって最適化することができます。それらを見つけて、Cython で書き直し、拡張モジュールにコンパイルして、拡張モジュール内の関数を呼び出します。

以上が99%の人は知らない! Python、C、C拡張、Cythonの違い比較!の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事は51cto.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。