`json.dumps`를 ~20% 더 빠르게 만든 방법

3848 단어 jsonpython
FastAPI 서비스의 성능과 동시성을 이해하기 위한 여정은 객체 트리에서 JSON 문자열을 생성하는 Pythonjson.dumps 함수로 이어졌습니다. 그리고 나는 그것을 조금 더 빠르게 만들었습니다.



_PyAccu 대 _PyUnicodeWriter



트리에 있는 모든 개체의 문자열 표현에서 JSON 문자열을 어떻게 생성합니까? json.dumps C 구현이 사용됨 _PyAccu for that .
_PyAccusmalllarge 의 두 문자열 목록을 유지합니다. 모든 문자열이 먼저 small 목록에 추가됩니다. 100,000개(!!!) 요소가 포함되면 모두 결합되어 결과가 large 목록에 추가됩니다. 이에 대한 Python 의사 코드는 다음과 같습니다.

large = []
small = []
while True:
    small.append(some_string)
    if len(small) == 100000:
        large.append(''.join(small))
        del small[:]
large.append(''.join(small))
return large



이 알고리즘에 대한 나의 초기 반응은 부정적이었습니다. 많은 작은 객체를 축적하고 나중에 모두 폐기하기 때문입니다.

대부분의 소프트웨어에서 메모리 할당이 작동하는 방식은 메모리 사용량을 쉽게 증가시키지만 절대 감소하지 않습니다. 메모리가 해제되더라도 여전히 프로세스를 위해 예약되어 있습니다. 메모리의 "꼬리"가 더 이상 필요하지 않은 경우 운영 체제로 메모리를 다시 해제할 수 있는 몇 가지 조건과 할당자가 있습니다. 그러나 그것들은 예외이지 표준이 아닙니다. 또한 많은 작은 개체가 해제되면 일부 할당자는 해제된 공간을 더 큰 영역으로 결합하려고 시도하며 시간이 걸립니다.

알고리즘 관점에서 볼 때 100,000개의 모든 문자열을 결합해야 하는 경우 코드는 두 번 통과합니다. 한 번은 총 크기를 계산하고 두 번째는 내용을 결과 문자열에 복사합니다. 이렇게 하면 각 개체를 뒤쫓는 CPU 캐시가 폐기되지만 해석된 Python 코드에는 큰 타격이 되지 않을 수 있습니다.

최신 버전의 Python에는 an alternative solution for that 이 있습니다. 저는 접근 방식_PyUnicodeWriter이 마음에 들며 이것이 제가 가장 선호하는 솔루션이 될 것입니다. 알고리즘은 매우 간단합니다. 새 문자열을 결과 문자열에 추가하기만 하면 됩니다.

result = ''
while True:
    result += some_string
return result



여기서 주요 요령은 결과 문자열에 필요한 메모리를 과도하게 할당하는 것입니다. 그렇지 않으면 매번 새 문자열result이 생성되고 이전 문자열resultsome_string의 내용이 복사됩니다. 초과 할당을 사용하면 result 문자열에 약간의 추가 공간이 있고 콘텐츠 재할당 및 복사가 매번 발생하지 않습니다. 나는 매번 크기를 두 배로 늘리지만 CPython은 Linux에서 25%, Windows에서 50%까지 크기를 늘립니다.
_PyAccu_PyUnicodeWriter로 교체하기 위해 변경했으며 pyperformance 벤치마크에서도 약간의 성능 향상을 가져왔습니다. 알고보니 _PyAccu는 전체 코드 베이스에서 두 곳에서만 사용되었기 때문에 모든 곳에서 _PyAccu를 교체하고 완전히 삭제했습니다. json.dumps가 더 빨라졌을 뿐만 아니라 CPython은 최소 100줄의 코드로 더 작아졌습니다.

기본 케이스의 빠른 경로


json.dumps의 C 구현은 출력에 영향을 미치는 몇 가지 매개변수를 사용합니다. 그 중 하나는 sort_keys=False to request sorting of dictionary keys 입니다. 이 기능의 구현은 Python 의사 코드에서 다음과 같습니다.

items = list(pydict.items())
if sort_keys:
    items.sort()
for key, value in items:
    ...


sort_keys가 기본적으로 False라는 점을 염두에 두고 코드를 주의 깊게 살펴보면 키와 값 쌍을 포함하는 튜플 목록이 생성되었음을 알 수 있습니다. 정렬이 수행되지 않으면 낭비되는 것 같습니다. 그래서 복사본을 만들지 않고 직접 사전 항목을 반복하는 별도의 코드 경로를 추가했습니다. 어느 것이 쉬웠고 주요 과제는 두 코드 경로 사이에서 루프 본문을 재사용하는 것이었습니다.

if sort_keys:
    items = list(pydict.items())
    items.sort()
    for key, value in items:
        ...
else:
    for key, value in pydict.items():
        ...



이 변경으로 인해 pyperformance 벤치마크가 10% 이상 향상되었습니다.

이것은 두 가지 좋은 변화였습니다. 나는 약간 더 복잡한 코드의 가치가 없는 것으로 밝혀진 몇 가지 아이디어를 가지고 있었습니다. Python 코드의 실행 시간과 비교할 때 개선 사항은 미미했고 소음과 구분하기 어려웠습니다.

이제 남은 것은 Python 3.12.0이 이러한 변경 사항과 함께 릴리스되는 2023-10-02를 기다리는 것입니다.

좋은 웹페이지 즐겨찾기