paste
은 훌륭한 도구이지만 실행 시 서버에서 약 50MB/s로 매우 느립니다.
paste -d, file1 file2 ... file10000 | pv >/dev/null
paste
에 따르면 100% CPU를 사용하므로 top
디스크 속도 저하 등의 요인에 의해 제한되지 않습니다.
소스 코드를 보면 다음을 사용하기 때문일 수 있습니다 getc
.
while (chr != EOF)
{
sometodo = true;
if (chr == line_delim)
break;
xputchar (chr);
chr = getc (fileptr[i]);
err = errno;
}
동일한 작업을 수행하지만 더 빠르게 수행할 수 있는 다른 도구가 있습니까? 한 번에 4k-64k 청크를 읽을 수 있을까요? 한 번에 한 바이트씩 보는 대신 벡터 명령어를 사용하여 병렬로 개행 문자를 찾는 것이 가능할까요? 아마 사용하거나 awk
비슷한가요?
입력 파일은 UTF8이고 너무 커서 RAM에 맞지 않으므로 읽기모든 것메모리에 들어가는 것은 선택 사항이 아닙니다.
편집하다:
Thanasisp에서는 작업을 병렬로 실행할 것을 권장합니다. 이렇게 하면 처리량이 약간 향상되지만 여전히 pure보다 속도가 훨씬 느립니다 pv
.
# Baseline
$ pv file* | head -c 10G >/dev/null
10.0GiB 0:00:11 [ 897MiB/s] [> ] 3%
# Paste all files at once
$ paste -d, file* | pv | head -c 1G >/dev/null
1.00GiB 0:00:21 [48.5MiB/s] [ <=> ]
# Paste 11% at a time in parallel, and finally paste these
$ paste -d, <(paste -d, file1*) <(paste -d, file2*) <(paste -d, file3*) \
<(paste -d, file4*) <(paste -d, file5*) <(paste -d, file6*) \
<(paste -d, file7*) <(paste -d, file8*) <(paste -d, file9*) |
pv | head -c 1G > /dev/null
1.00GiB 0:00:14 [69.2MiB/s] [ <=> ]
top
여전히 외부 소스가 paste
병목 현상을 일으키는 것으로 나타납니다.
버퍼를 늘리면 차이가 있는지 테스트했습니다.
$ stdbuf -i8191 -o8191 paste -d, <(paste -d, file1?) <(paste -d, file2?) <(paste -d, file3?) <(paste -d, file4?) <(paste -d, file5?) <(paste -d, file6?) <(paste -d, file7?) <(paste -d, file8?) <(paste -d, file9?) | pv | head -c 1G > /dev/null
1.00GiB 0:00:12 [80.8MiB/s] [ <=> ]
이로 인해 처리량이 10% 증가합니다. 버퍼를 더 늘려도 아무것도 개선되지 않았습니다. 이는 하드웨어와 관련이 있을 수 있습니다(즉, 레벨 1 CPU 캐시의 크기 때문일 수 있음).
테스트는 디스크 하위 시스템과 관련된 제한을 피하기 위해 RAM 디스크에서 실행됩니다.
답변1
다양한 대안과 시나리오를 사용한 추가 테스트
(편집: 컴파일된 버전에 대한 컷오프 값 추가)
간단히 말해서:
- 예, coreutils
paste
가 더 좋습니다cat
paste
특히 많은 양의 짧은 줄의 경우 더 빠른 coreutils 대신 쉽게 사용할 수 있는 대안은 없는 것 같습니다.paste
처리량은 라인 길이, 라인 수 및 파일 수의 다양한 조합에서 놀랍도록 안정적입니다.- 더 긴 노선의 경우 더 빠른 대안이 아래에 제공됩니다.
상세히:
많은 시나리오를 테스트했습니다. 처리량 측정은 pv
원래 게시물에서와 같이 수행되었습니다.
비교 옵션:
cat
(기준으로 GNU coreutils 8.25에서)paste
(또한 GNU coreutils 8.25에서)- 파이썬 스크립트~에서위에서 답변함
- 대체 Python 스크립트(행 조각을 수집하기 위한 목록 이해를 일반 루프로 대체)
- 프로젝트 님(4와 비슷하지만 실행 파일로 컴파일됨)
파일/라인 번호 조합:
# | 목록 | 철사 |
---|---|---|
1 | 200,000 | 1,000 |
2 | 20,000 | 10,000 |
삼 | 2,000 | 100,000 |
4 | 200 | 1,000,000 |
5 | 20 | 10,000,000 |
6 | 2 | 100,000,000 |
각 테스트의 총 데이터 용량은 1.3GB로 동일했습니다. 각 열은 6자리 숫자(예: 000'001 ~ 200'000)로 구성됩니다. 위의 조합은 1, 10, 100, 1'000, 10'000개의 동일한 크기의 파일에 최대한 많이 배포됩니다.
생성된 파일은 다음과 같습니다.yes {000001..200000} | head -1000 > 1
붙여넣기는 다음과 같이 수행됩니다.for i in cat paste ./paste ./paste2 ./paste3; do $i {00001..1000} | pv > /dev/null; done
그러나 붙여넣은 파일은 실제로 모두 동일한 원본 파일에 대한 링크이므로 모든 데이터는 어쨌든 캐시에 있어야 합니다(붙여넣기 직전에 생성되어 먼저 읽혀짐 cat
; 시스템 메모리는 128GB, 캐시 크기 34GB)
미리 생성된 파일에서 읽어서 파이프로 연결하는 대신 데이터를 즉시 생성하는 또 다른 세트가 실행되었습니다 paste
(아래 표시, 파일 수 = 0).
마지막 명령 세트의 경우 다음과 같습니다.for i in cat paste ./paste ./paste2 ./paste3; do $i <(yes {000001..200000} | head -1000) | pv > /dev/null; done
발견하다:
paste
수십배 느린 속도cat
paste
처리량은 다양한 선 너비와 관련 파일 수에 걸쳐 매우 일정합니다(약 300MB/s).- 평균 입력 파일 줄 길이가 특정 제한(내 테스트 컴퓨터에서는 약 1400자)을 초과하면 집에서 만든 Python 대안이 몇 가지 이점을 보여줄 수 있습니다.
- Python 스크립트와 비교하여 컴파일된 nim 버전은 처리량이 약 두 배입니다. 이에 비해
paste
입력 파일의 손익분기점은 약 500자입니다. 이 값은 입력 파일 수가 증가함에 따라 감소하며, 입력 파일이 10개 이상 포함되면 입력 파일 줄당 약 150자까지 줄어듭니다. - Python과 nim 버전 모두 많은 짧은 줄에 대한 처리 오버헤드로 인해 어려움을 겪고 있습니다(의심스러운 원인: 사용된 두 stdlib 함수에서 줄 끝을 감지하고 이를 플랫폼별 끝으로 변환하려는 시도). 그러나 coreutils는
paste
영향을 받지 않습니다. cat
동시 동적 데이터 생성 프로세스는 행이 더 긴 nim 버전의 제한 요소이며 처리 속도에도 어느 정도 영향을 미치는 것으로 보입니다 .- 어떤 시점에서는 많은 수의 열린 파일 핸들이 coreutils에 부정적인 영향을 미치는 것처럼 보입니다
paste
. (그냥 추측입니다. 어쩌면 이것이 병렬 버전에도 영향을 미칠까요?)
결론(적어도 테스트 머신의 경우)
- 입력 파일이 좁은 경우, 특히 파일이 긴 경우 coreutils Paste를 사용하십시오.
- 입력 파일이 상당히 넓은 경우 대안이 선호됩니다(입력 파일 수에 따라 Python 버전의 경우 입력 파일 줄 길이 > 1400자, nim 버전의 경우 150-500자).
- 일반적으로 Python 스크립트 대신 컴파일된 nim 버전을 선호합니다.
- 작은 조각이 너무 많으면 주의하세요. 이 경우 프로세스에 대한 기본 소프트 제한인 1024개의 열린 파일은 상당히 합리적으로 보입니다.
OP 상황에 대한 제안(병렬 처리)
입력 파일이 좁은 경우 paste
내부 작업에서 coreutils를 사용하고컴파일된 대안가장 바깥쪽 프로세스의 경우. 모든 파일에 긴 줄이 있으면 일반적으로 nim 버전을 사용하십시오.
동굴: 링크된 프로그램은 임시 릴리스이며 어떠한 보증이나 명시적인 오류 처리 없이 "있는 그대로" 제공됩니다. 또한 세 가지 구현 모두에서 구분 기호가 하드코딩되어 있습니다.
답변2
getc는 고도로 최적화되어 있으며 현재 주로 버퍼 지원 매크로로 구현되어 있지만 여전히 병목 현상이 발생할 수 있다는 점에 동의합니다. 또는 오히려 사용되는 버퍼의 크기가 상대적으로 작을 수 있으므로 여전히 높은 결과를 낳을 수 있습니다. 파일 읽기 횟수.
위에 표시된 정확한 수치를 얻지는 못했지만 테스트에서 기준 실행과 붙여넣기 실행 사이에는 여전히 눈에 띄는 차이가 있었습니다.
(어쩌면 범인은 버퍼링(파일 블록, 스트림 라인)의 스위치일 수도 있습니다. 하지만 저는 그런 경험이 없습니다.)
추가 테스트를 통해 다음 구조를 사용할 때 처리량이 비슷하게 감소하는 것을 확인했습니다.
cat file* | dd | pv > /dev/null
중간 인서트 dd의 처리량은 페이스트 실행의 처리량과 매우 유사합니다. (dd는 기본적으로 512바이트의 블록 크기를 사용합니다). 버퍼 크기를 더 줄이면 그에 비례하여 실행 시간도 늘어납니다. 그러나 블록 크기를 단지 몇 kb(예: 8 또는 16)로 늘리면 속도가 크게 증가합니다. 1 또는 2M을 사용할 때 이륙했습니다.
cat file* | dd bs=2M | pv > /dev/null
getc의 버퍼 크기를 변경하는 것이 가능한 것 같습니다(stdout이 라인 버퍼링에서 자유롭게 선택한 크기의 블록 버퍼링으로 전환되는 경우). 그러나 열려 있는 파일이 수천 개이고 각 파일에 버퍼가 있는 경우 메모리 요구 사항이 빠르게 증가할 수 있다는 점을 기억해야 합니다.
그럼에도 불구하고 버퍼링을 변경해 볼 수 있습니다(예:https://stackoverflow.com/questions/66219179/usage-of-getc-with-a-file) 적절한 setvbuf 호출을 사용하여 무슨 일이 일어나는지 확인하세요.
다음에 추가: 현재 동일한 작업을 더 빠르게 수행하는 유사한 공개 프로그램을 알지 못합니다.
(PS: 이름을 방금 봤습니다. GNU 병렬 전문가이신가요? 수고하셨습니다!)
답변3
나는 과거에 매우 빠른 Python 텍스트 처리(즉, 고도로 최적화되고 고도로 조정된 Python 텍스트 처리 루틴)를 경험했기 때문에 작은 q&d Python 스크립트를 함께 던져서 coreutils의 붙여넣기와 비교했습니다. (물론 이 옵션은 데모용으로만 사용되며(현재 파일의 파일 이름만 허용) 열 구분 기호가 내장되어 있기 때문에 옵션이 매우 제한적입니다.)
현재 디렉터리에 있는 텍스트 파일 에 넣고 paste
실행 권한을 부여한 후 시도해 보세요.
#! /usr/bin/env python3
import sys
filenames = sys.argv[1:]
infiles = [open(i,'r') for i in filenames]
while True:
lines = [i.readline() for i in infiles]
if all([i=='' for i in lines]):
break
print("\t".join([i.strip("\n") for i in lines]))
고쳐 쓰다:모든 입력 파일에서 동일한 행이 비어 있는 것으로 발견되면 스크립트가 즉시 중단되도록 하는 위 스크립트의 버그를 수정했습니다. Python 버전의 타이밍 측정도 실제 실행 시간을 반영하도록 아래에 업데이트되었습니다.
여러 시스템에서 캐시를 워밍업한 후(램디스크를 생성할 필요 없이) 테스트했습니다.위의 Python 버전은 항상 coreutils Paste보다 낫습니다.범위는 10%(기본 Debian Buster)부터 30%(Debian Stretch를 실행하는 VM)입니다. Windows에서는 차이가 더 눈에 띄지만 이는 추가적인 posix 변환 오버헤드 또는 다른 캐싱으로 인해 발생할 수 있습니다(coreutils Paste 8.26을 사용하는 cygwin: >20x 느림; coreutils Paste 8.32를 사용하는 msys2: Python 버전 시간보다 >12x 느림; 둘 다 Python을 실행함). 3.9.9) 이 테스트에서는 긴 줄을 처리하는 데 문제가 있는 것 같았기 때문에 매우 긴 줄 100개가 포함된 파일을 만들어 파일 자체에 붙여넣었습니다.
jf1@s1 MSYS /d/temp/ui
# time paste b b > /dev/null
real 0m5.920s
user 0m5.896s
sys 0m0.031s
jf1@s1 MSYS /d/temp/ui
# time ../paste b b > /dev/null
real 0m0.480s
user 0m0.295s
sys 0m0.170s
jf1@s1 MSYS /d/temp/ui
# ../paste b b > c1
jf1@s1 MSYS /d/temp/ui
# paste b b > c2
jf1@s1 MSYS /d/temp/ui
# diff c1 c2
jf1@s1 MSYS /d/temp/ui
#