>  기사  >  백엔드 개발  >  99%의 사람들은 모른다! Python, C, C 확장, Cython 차이점 비교!

99%의 사람들은 모른다! Python, C, C 확장, Cython 차이점 비교!

WBOY
WBOY앞으로
2023-04-14 17:40:031887검색

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는 내부적으로 double로 저장되는 맨 아래 레이어의 PyFloatObject에 해당한다는 것입니다.

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을 사용하여 Fibonacci를 작성하는 방법을 살펴보겠습니다. 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의 구문을 학습하지는 않았지만 위 코드가 무엇을 의미하는지 짐작할 수 있을 것입니다. cdef 키워드를 사용하여 C 수준 변수를 정의하고 해당 유형을 선언했습니다.

Cython 코드는 인터프리터가 인식하기 전에 확장 모듈로 컴파일해야 하므로 먼저 C 코드로 변환한 다음 확장 모듈로 컴파일해야 합니다. 다시 말하지만, C 확장을 작성하는 것과 Cython을 작성하는 것 사이에는 본질적으로 차이가 없습니다. Cython 코드도 C 코드로 변환해야 합니다.

하지만 분명히 Cython을 작성하는 것은 C 확장을 작성하는 것보다 훨씬 간단합니다. 작성된 Cython 코드의 품질이 매우 높으면 번역된 C 코드의 품질도 매우 높을 것이며 최대 코드가 자동으로 처리됩니다. 번역 과정 중 최적화 정도. 그러나 손으로 작성한 C 확장인 경우 모든 최적화는 개발자가 수동으로 처리해야 하며, 기능이 복잡할 경우 C 확장을 작성하는 것 자체가 골치 아픈 일이라는 점은 말할 것도 없습니다.

Cython의 속도를 높일 수 있는 이유는 무엇인가요?

Cython 코드를 보면 순수 Python Fibonacci와 비교하면 변수 i, a, b의 유형이 미리 지정되어 있다는 점이 차이점인 것 같습니다. 이것이 속도를 높일 수 있는 이유입니다. 효과는 어떻습니까(아직 테스트되지 않았지만 속도는 확실히 증가할 것입니다. 그렇지 않으면 Cython을 배울 필요가 없습니다).

하지만 그 이유는 Python의 모든 변수가 일반 포인터 PyObject *이기 때문입니다. PyObject(C의 구조)에는 두 개의 내부 멤버가 있습니다. 즉 ob_refcnt: 객체의 참조 카운트를 보유하고, ob_type *: 객체 유형의 포인터를 보유합니다.

정수, 부동 소수점 숫자, 문자열, 튜플, 사전 또는 기타 무엇이든 이를 가리키는 모든 변수는 PyObject *입니다. 동작 시에는 먼저 ->ob_type을 통해 해당 타입의 포인터를 얻은 후 변환을 수행해야 합니다.

예를 들어 Python 코드의 a와 b는 어떤 루프 수준이 수행되든 결과가 부동 소수점 숫자를 가리킨다는 것을 알고 있지만 해석기는 이러한 추론을 하지 않습니다. 각 추가를 감지하여 해당 유형을 결정한 다음 추가를 수행할 때 내부 __add__ 메서드로 이동하여 실행이 완료된 후 새 개체를 만듭니다. PyObject *로 이동하고 반환합니다.

그리고 Python 객체는 힙에 공간을 할당하고 a와 b는 변경할 수 없으므로 매 주기마다 새 객체를 생성하고 이전 객체를 재활용합니다.

위의 모든 결과로 인해 Python 코드의 높은 실행 효율성이 불가능해졌습니다. Python도 메모리 풀과 해당 캐싱 메커니즘을 제공하지만 여전히 낮은 효율성을 견딜 수는 없습니다.

Cython이 가속할 수 있는 이유에 대해서는 나중에 이야기하겠습니다.

효율 차이

그럼 둘 사이의 효율성 차이는 무엇일까요? 표를 사용하여 비교해 보겠습니다.

99%의 사람들은 모른다! Python, C, C 확장, Cython 차이점 비교!

개선 계수는 순수 Python에 비해 효율성이 몇 배나 향상되었는지를 나타냅니다.

두 번째 열은 fib(0)입니다. 물론 fib(0)은 함수 호출 비용을 측정합니다. 두 번째 열 "루프 본문 시간 소비"는 함수 호출 자체의 오버헤드를 제외하고 fib(90) 실행 시 내부 루프 본문을 실행하는 데 소요된 시간을 나타냅니다.

전체적으로 순수 C 언어로 작성된 피보나치가 가장 빠르기는 하지만, 생각해 볼 만한 점이 많이 있습니다.

Pure Python

역시 모든 면에서 최악의 성능을 보이는 녀석입니다. fib(0)으로 판단하면 함수를 호출하는 데 590나노초가 걸리며 이는 C보다 훨씬 느립니다. 그 이유는 Python이 함수를 호출할 때 스택 프레임을 생성해야 하는데 이 스택 프레임이 힙에 할당되고 그 이후에 발생하기 때문입니다. 결국에는 스택 프레임 파괴 등도 포함됩니다. fib(90)의 경우 당연히 분석이 필요하지 않습니다.

Pure C

분명히 현재로서는 Python 런타임과 상호 작용이 없으므로 성능 소비가 최소화됩니다. fib(0)은 C에서 함수를 호출하는 데 2나노초밖에 걸리지 않음을 보여줍니다. fib(90)은 루프 실행 시 C가 Python보다 거의 80배 빠르다는 것을 보여줍니다.

C 확장

위에서 언급했듯이 C 확장이 하는 일은 C를 사용하여 Python용 확장 모듈을 작성하는 것입니다. 루프 본문의 시간 소비를 살펴보면 C 확장이 순수 C와 거의 동일하다는 것을 알 수 있습니다. 차이점은 함수 호출에 더 많은 시간이 소요된다는 것입니다. 그 이유는 확장 모듈의 함수를 호출할 때 먼저 Python 데이터를 C 데이터로 변환한 다음 C 함수를 사용하여 피보나치 수열을 계산하고 C 데이터를 Python 데이터로 변환해야 하기 때문입니다.

그래서 C 확장은 본질적으로 C 언어이지만 작성할 때 CPython에서 제공하는 API 사양을 따라야 C 코드를 pyd 파일로 컴파일하고 Python에서 직접 호출할 수 있습니다. 결과적으로 보면 Cython과 동일합니다. 하지만 다시 말하지만, C로 확장을 작성하는 것은 본질적으로 C를 작성하는 것이며, 상대적으로 어려운 기본 Python/C API에도 익숙해야 합니다.

Cython

루프 본문만 보면 시간이 많이 걸리는 것을 보면 순수 C, C 확장, Cython 모두 거의 같지만 Cython을 작성하는 것이 확실히 가장 편리합니다. Cython이 수행하는 작업은 본질적으로 C 확장과 유사합니다. 둘 다 Python용 확장 모듈을 제공합니다. 차이점은 하나는 C 코드를 수동으로 작성하는 것이고, 다른 하나는 Cython 코드를 작성한 후 자동으로 C 코드로 변환하는 것입니다. 따라서 사이썬의 경우 파이썬 데이터를 C 데이터로 변환하고, 계산을 수행한 후, 다시 파이썬 데이터를 변환하는 과정이 불가피하다.

그러나 Cython은 C 확장보다 함수를 호출하는 데 훨씬 적은 시간이 소요됩니다. 주된 이유는 Cython에서 생성된 C 코드가 고도로 최적화되어 있기 때문입니다. 하지만 솔직히 말해서 함수 호출에 걸리는 시간은 크게 신경 쓸 필요가 없습니다. 우리가 주목해야 할 것은 내부 코드 블록이 실행되는 데 걸리는 시간입니다. 물론 나중에 함수 호출 자체의 오버헤드를 줄이는 방법에 대해서도 이야기하겠습니다.

Python의 for 루프가 왜 이렇게 느린가요?

루프 본문의 시간 소모를 보면 Python의 for 루프가 정말 느리기로 악명 높다는 것을 알 수 있는데, 그 이유는 무엇일까요? 분석해보자.

1. Python의 for 루프 메커니즘

Python은 반복 가능한 객체 내에서 먼저 __iter__ 메서드를 호출하여 해당 반복자를 반환한 다음 반복자의 __next__ 메서드를 계속 호출합니다. 반복자가 StopIteration 예외를 발생시킬 때까지 값을 하나씩 반복합니다. 이 예외는 for 루프에 의해 캡처되어 루프를 종료합니다.

그리고 반복자는 상태를 저장하며 Python 인터프리터는 항상 반복자의 반복 상태를 기록해야 합니다.

2. Python의 산술 연산

실제로 위에서 언급한 바 있습니다. Python은 자체 동적 특성으로 인해 유형 기반 최적화를 수행할 수 없습니다.

예: 루프 본문의 a + b, 이 a와 b는 정수, 부동 소수점 숫자, 문자열, 튜플, 목록 또는 심지어 매직 메소드 __add__를 구현한 클래스의 인스턴스 객체를 가리킬 수 있습니다. 기타 등등

부동 소수점 숫자라는 것을 알지만 Python에서는 이러한 가정을 하지 않으므로 a + b가 실행될 때마다 해당 유형은 무엇입니까? 그런 다음 내부적으로 __add__ 메서드가 있는지 확인합니다. 그렇다면 a 및 b를 매개 변수로 사용하여 호출하여 a 및 b가 가리키는 개체를 추가합니다. 결과가 계산된 후 해당 포인터가 PyObject *로 변환되어 반환됩니다.

C와 Cython의 경우 변수 생성시 다른 것이 아닌 double로 미리 타입을 지정해주기 때문에 컴파일된 a+b는 단순한 기계 명령어일 뿐입니다. 이에 비해 Python이 어떻게 느리지 않을 수 있습니까?

3. Python 개체의 메모리 할당

Python 개체는 본질적으로 C의 malloc 함수에 의해 구조의 힙 영역에 할당된 메모리 조각이기 때문에 힙에 할당됩니다. 힙 영역에 메모리를 할당하고 해제하려면 많은 돈이 필요하지만 스택이 훨씬 작고 운영 체제에 의해 유지되며 자동으로 재활용되므로 스택에서만 메모리를 할당하고 해제하는 것이 매우 효율적입니다. 한 번의 이동만 필요합니다.

하지만 힙에는 분명히 이러한 처리가 없으며 Python 개체는 모두 힙에 할당됩니다. Python은 운영 체제와의 빈번한 상호 작용을 어느 정도 방지하기 위해 메모리 풀 메커니즘을 도입하고 작은 정수 개체도 도입합니다. 풀, 문자열 인턴 메커니즘, 캐시 풀 등

그러나 실제로 객체(스칼라를 포함한 모든 객체)의 생성 및 소멸과 관련하여 동적으로 할당된 메모리와 Python의 메모리 하위 시스템의 오버헤드가 증가합니다. float 객체는 불변 객체이므로 루프를 돌 때마다 생성되고 소멸되므로 여전히 효율성이 높지 않습니다.

Cython에서 할당한 변수(유형이 C의 유형인 경우)는 더 이상 포인터가 아닙니다(Python 변수는 모두 포인터입니다). 현재 a와 b의 경우 스택에 할당됩니다. 배정밀도 부동 소수점 숫자. 스택에서의 할당 효율성은 힙의 할당 효율성보다 훨씬 높기 때문에 for 루프에 매우 적합하므로 효율성이 Python보다 훨씬 높습니다. 또한 할당뿐만 아니라 주소 지정 시에도 스택이 힙보다 효율적입니다.

따라서 for 루프의 경우 C와 Cython이 순수 Python보다 훨씬 빠른 것은 놀라운 일이 아닙니다. Python은 각 반복에서 많은 작업을 수행하기 때문입니다.

Cython은 언제 사용하나요?

Cython 코드에서는 CDEF 몇 개만 추가하면 성능이 크게 향상될 수 있다는 점을 알 수 있습니다. 이는 분명히 매우 흥미로운 일입니다. 그러나 모든 Python 코드가 Cython으로 작성될 때 성능이 크게 향상되는 것은 아닙니다.

여기에 있는 피보나치 수열 예제는 내부 데이터가 CPU에 바인딩되어 있고 데이터를 이동할 필요 없이 CPU 레지스터의 일부 변수를 처리하는 데 런타임이 소비되기 때문에 의도적인 것입니다. 이 함수가 다음 작업을 수행하는 경우:

  • 대규모 배열에 요소 추가와 같은 메모리 집약적
  • I/O 집약적(예: 디스크에서 대용량 파일 읽기) 디스크 FTP 서버에서 파일을 다운로드하면
  • 그러면 Python, C, Cython 간의 차이점이 크게 줄어들거나(저장소 집약적 작업의 경우) 완전히 사라질 수도 있습니다(I/O 집약적 또는 네트워크의 경우). - 집중적인 작업).

Python 프로그램의 성능을 향상시키는 것이 우리의 목표일 때 Pareto 원칙은 우리에게 많은 도움이 됩니다. 즉, 프로그램 실행 시간의 80%가 코드의 20%에서 발생합니다. 그러나 신중한 분석 없이는 이러한 20%의 코드를 찾기가 어렵습니다. 따라서 Cython을 사용하여 성능을 향상시키기 전에 전반적인 비즈니스 로직을 분석하는 것이 첫 번째 단계입니다.

분석을 통해 프로그램의 병목 현상이 네트워크 IO로 인해 발생한다고 판단한다면 Cython이 상당한 성능 향상을 가져올 것이라고 기대할 수는 없습니다. 따라서 Cython을 사용하기 전에 먼저 프로그램에서 병목 현상을 일으키는 원인이 무엇인지 파악하는 것이 필요합니다. 따라서 Cython은 강력한 도구이기는 하지만 올바른 방식으로 사용해야 합니다.

또한 Cython은 C의 유형 시스템을 Python에 도입했기 때문에 C의 데이터 유형의 제한 사항에 주의해야 합니다. 우리는 Python의 정수가 길이에 의해 제한되지 않는다는 것을 알고 있지만 C의 정수는 제한되어 있으므로 무한 정밀도 정수를 올바르게 표현할 수 없습니다.

그러나 Cython의 일부 기능은 이러한 오버플로를 포착하는 데 도움이 될 수 있습니다. 간단히 말해서, 가장 중요한 것은 C 데이터 유형이 Python 데이터 유형보다 빠르지만 제한적이며 유연하고 다재다능하지 않다는 것입니다. 여기에서 우리는 Python이 속도, 유연성 및 다양성 측면에서 후자를 선택했음을 알 수 있습니다.

또한 Cython의 또 다른 기능인 외부 코드 연결을 고려해보세요. 시작점이 Python이 아니라 C 또는 C++이고 Python을 사용하여 여러 C 또는 C++ 모듈을 연결한다고 가정합니다. Cython은 C 및 C++ 선언을 이해하고 고도로 최적화된 코드를 생성할 수 있으므로 브리지로 더 적합합니다.

저는 Python 마스터이므로 C와 C++를 포함한다면 Cython에 C와 C++를 도입하고 이미 작성된 C 라이브러리를 직접 호출하는 방법을 소개하겠습니다. 여러 C 및 C++ 모듈을 연결하는 브리지로 Cython을 C 및 C++에 도입하는 방법은 소개하지 않습니다. 나는 C나 C++를 사용하여 서비스를 작성하지 않고 Python의 효율성 향상을 지원하는 데에만 C나 C++를 사용하기 때문에 이 점을 이해하시기 바랍니다.

요약

지금까지는 Cython에 대해서만 소개했고 Python과 C와의 포지셔닝과 차이점을 주로 다루었습니다. Cython을 사용하여 Python을 가속화하는 방법, Cython 코드 작성 방법 및 자세한 구문에 대해서는 나중에 소개하겠습니다.

간단히 말하면 Cython은 Python을 제공하는 성숙한 언어입니다. Cython 코드는 Python의 구문 규칙을 따르지 않기 때문에 직접 실행할 수 없습니다.

Cython을 사용하는 방법은 먼저 Cython 코드를 C 코드로 변환한 다음 C 코드를 확장 모듈(pyd 파일)로 컴파일한 다음 Python 코드로 가져오고 내부의 함수형 메서드를 호출하는 것입니다. 우리가 하는 일 Cython을 사용하는 올바른 방법이자 유일한 방법입니다.

예를 들어, 위의 Cython에서 작성한 Fibonacci는 직접 실행하면 오류를 보고합니다. 왜냐하면 cdef는 분명히 Python의 구문 규칙을 따르지 않기 때문입니다. 따라서 Cython 코드를 확장 모듈로 컴파일한 다음 일반 py 파일로 가져와야 합니다. 이는 실행 속도를 향상시키는 것입니다. 따라서 Cython 코드는 CPU 집약적인 코드여야 하며, 그렇지 않으면 효율성을 크게 향상시키기 어려울 것입니다.

그러므로 Cython을 사용하기 전에 비즈니스 로직을 주의 깊게 분석하거나, 당분간 Cython을 사용하지 않고 Python으로 완전히 작성하는 것이 가장 좋습니다. 작성이 완료된 후 프로그램 성능 테스트 및 분석을 시작하여 어디에서 시간이 더 걸리는지 확인하지만 동시에 정적 타이핑을 통해 최적화할 수 있습니다. 이를 찾아 Cython에서 다시 작성하고 확장 모듈로 컴파일한 다음 확장 모듈에서 함수를 호출합니다.

위 내용은 99%의 사람들은 모른다! Python, C, C 확장, Cython 차이점 비교!의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 51cto.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제