직접 Linux 커널 호출과 POSIX 지정 함수 호출 성능

직접 Linux 커널 호출과 POSIX 지정 함수 호출 성능

안에스택 오버플로에 대한 답변, 질문에 언급된 몇 가지 작은 작업을 수행하기 위한 코드 예제를 제공했습니다. 원래 질문은 어떤 기술이 가장 빠르게 수행되는지와 관련이 있었습니다(따라서 여기에서 성능 기준이 적용됩니다).

또 다른 의견 제시자/답변에서는 POSIX 정의 시스템 API 호출(이 경우 readdir) 을 만드는 대신에 제안했습니다.직접시스템이 커널( syscall(SYS_getdents,...))을 호출하고 성능 차이가 25% 범위에 있다고 주장합니다. (구현하거나 다시 벤치마킹하지는 않았습니다. 실제로 성능이 더 좋아질 수 있다고 생각합니다.)

제 질문은 제안된 시스템 호출 기반 솔루션의 성능 특성에 관한 것입니다.더 빠를 수도 있습니다. 성능이 더 좋은 몇 가지 이유를 생각해 볼 수 있습니다.

  1. POSIX는 readdir본질적으로 syscall(SYS_getdents,...)/보다 더 복잡 합니다.getdents()
  2. readdir(아마도 호출은 syscall(SYS_getdents,...)간접적인 오버헤드를 추가할 뿐입니다.
  3. readdir(커널 호출당) 하나의 레코드만 반환하는 반면, syscall(SYS_getdents,...)/getdents()`는 (아마도) 커널 호출당 여러 레코드를 반환합니다.

위의 #1이 사실이라고는 상상할 수 없습니다. glibc의 구현은 direct보다 더 많은 "실제" 시스템 호출을 호출 / 호출할 수 없을 정도로 유사 readdir합니다 .getdentsreaddirsyscall(SYS_getdents,...)getdents()

readdir또한 호출이 래핑될 수 있고 호출 getdents도 가능 하기 때문에 #2가 사실이라고 상상할 수 없습니다 (제안된 답변은 직접 호출하는 대신 구체적으로 사용함). Linux의 glibc에 있는 모든 것은 아마도 이 경우 #2 로 귀결될 것입니다 .syscall(SYS_getdents,...)getdentssyscall(SYS_getdents,...)getdentssyscall(syscallid, args)진짜.

내 생각에는 마지막 가능성이 가장 좋은 설명입니다. 커널 호출이 적을수록 성능이 더 빨라집니다.

"직접 커널 호출"이 POSIX 정의 함수를 호출하는 것보다 훨씬 빠른 이유에 대한 구체적인 설명이 있습니까?

답변1

PLT이것이 Linux에서 가장 비용이 많이 드는 호출 중 하나라는 점을 고려하면 간접 참조 또는 가변 인수(레지스터는 메모리에 저장되어야 함) 와 같은 요소는 syscall()'s거의 영향을 미치지 않습니다.getdents

내 컴퓨터에서는 빈 디렉터리를 완전히 읽는 데 약 5μs가 걸리고, 100개 항목이 있는 디렉터리를 읽는 데는 37μs, 1000개 항목이 있는 디렉터리를 읽는 데는 340μs, 10,000개 항목이 있는 디렉터리를 읽는 데는 3.79ms가 걸립니다.

fdopendir+ readdir효과는 getdents버퍼 할당/사용 가능(0.1μs)을 추가하고 stat제공된 fd가 디렉터리 유형(0.4μs)인지 확인하는 것입니다. readdir그런 다음 각 디렉토리 항목에 대해 간단한 호출을 수행합니다(버퍼에서 한 위치를 이동하고 다시 채울 수 있음).

따라서 일회성 오버헤드는 0.5μs입니다. 이는 빈 디렉터리에 대한 디렉터리 스캔 시간의 10%이지만 100개 항목 디렉터리의 경우 1%에 불과하고 큰 디렉터리에서는 거의 무시할 수 있습니다. fdopen이 필요하지 않은 경우 이 오버헤드는 5배로 줄어듭니다(할당/무료 비용만 해당). ( diropen직접 사용할 수 없는 경우 fdopen만 필요하므로 (예: 'ted) 파일 디스크립터를 별도로 구해야 합니다 openat.)

따라서 사용자 정의 일회성 할당 버퍼를 사용하는 getdents경우비어 있는디렉토리이며 더 큰 디렉토리에서는 거의 무시할 수 있습니다.

호출 의 경우 readdirPLT 간접 비용은 최신 하드웨어에서 일반적으로 1ns 미만이며 함수 호출 오버헤드는 약 1-2ns입니다. 디렉터리 스캔 시간이 마이크로초 정도라는 점을 고려하면 readdir이러한 요소를 단일 µs로 변환하려면 최소 1000번의 호출이 필요하지만 스캔 비용은 340 µs이고 누적된 1 µs는 그 중 약 0.3%입니다. 영향은 다음과 같습니다. 무시할 수 있는 . 이를 인라인하면 readdir(따라서 호출 오버헤드와 PLT 오버헤드 제거) 코드 확장에만 도움이 되지만 getdents병목 현상이 발생하므로 성능이 크게 향상되지는 않습니다.

( 추가 잠금으로 인해 비용이 더 많이 들지만 일반 호출은 일반적으로 스레드로부터 안전하므로 readdir_r그럴 필요는 없습니다.readdir_rreaddir~하지 않는 한당신은 그들을 호출하는 여러 스레드가 있습니다동일한디렉터리 스트림. POSIX는 아직 이를 명시적으로 밝히지 않을 수도 있지만 glibc가 더 이상 사용되지 않는다는 점을 고려하면 이 보장이 곧 표준화되어야 한다고 생각합니다 readdir_r. )

답변2

및 친구와 같은 기능은 readdir()공유 라이브러리인 libc에서 구현됩니다. 모든 공유 라이브러리와 마찬가지로 공유 라이브러리 내 함수의 메모리 주소를 확인할 수 있도록 일부 리디렉션이 추가됩니다.

특정 라이브러리 호출이 처음 실행될 때 동적 링커는 해시 테이블 내에서 라이브러리 호출 주소를 찾아야 합니다. 여기에는 적어도 한 번 이상(아마도 그 이상) 문자열 비교가 포함되는데, 이는 상대적으로 비용이 많이 드는 접근 방식입니다. 찾은 주소는 PLT(프로시저 연결 테이블)에 저장되어 다음에 함수가 호출될 때 함수를 찾는 오버헤드가 3개의 명령어로 줄어듭니다(x86 아키텍처에서는 다른 아키텍처보다 적음). 그렇기 때문에 무언가를 공유 객체(정적 객체가 아닌)로 컴파일하면 약간의 오버헤드가 발생합니다. 공유 라이브러리 오버헤드와 Linux에서 공유 라이브러리가 작동하는 방식에 대한 자세한 내용은 다음을 참조하세요.주제에 대한 Ulrich Drepper의 자세한 기술 설명.

기능 syscall()자체는반품libc로 구현되므로 리디렉션 기능도 있습니다. 그러나 해당 함수만 사용하고 다른 함수는 사용하지 않으므로 동적 링커가 수행할 작업이 줄어듭니다. 또한 특정 함수의 구현은 예를 들어 함수 readdir종료 시 반환 값을 변환 하고 오류 검사 등을 수행해야 합니다. 이는 추가 오버헤드입니다. syscall()직접 실행되는 프로그램은 syscall()시스템 호출의 직접 반환 값을 사용하며 해당 변환이 필요하지 않습니다(여전히 오류 검사가 필요하므로 함수가 상당히 복잡해집니다).

직접 실행의 단점은 syscall()이식성이 떨어지는 API로 전환한다는 것입니다. 맨페이지에서는 syscall()libc가 처리하는 몇 가지 아키텍처별 제약 조건을 설명합니다. syscall()이를 직접 사용하면 함수가 작업 중인 아키텍처에서 실행될 수 있지만 ARM 시스템에서는 실패할 수 있습니다.

일반적으로 저는 syscall()어셈블리 언어로 직접 코드를 작성하는 것을 권장하지 않는 것과 같은 이유로 API를 직접 사용하지 않는 것을 권장합니다. 예, 이렇게 하면 속도가 더 빨라질 수 있지만 유지 관리 부담은 더 커질 것입니다. 당신이 할 수 있는 일이 몇 가지 있습니다:

  • 성능에는 신경쓰지 마세요. 시스템은 점점 더 저렴해지고 있으며, 많은 경우 "성능 향상을 위해 프로그래머에게 시간당 비용을 지불"하는 것보다 "작업 속도를 높이기 위해 다른 시스템을 추가"하는 것이 더 저렴합니다.
  • 공유 라이브러리를 사용하는 대신 정적 라이브러리에 대해 소프트웨어를 컴파일하는 등 성능에 중요한 몇 가지 작은 사항(예 gcc -static:)
  • 프로파일러를 사용하여 진행이 느린 부분을 확인하고 집중하세요.그것들시스템 호출 방법에 대해 걱정하는 대신에.

답변3

"직접 커널 호출"이 훨씬 더 빠른 이유에 대한 구체적인 설명이 있습니까?

이것이 실제로 중요하다고 생각하는 악명 높은 상황은 디렉토리의 크기와 주어진 파일 시스템이 미리 읽기를 충분히 수행하지 않는 경우입니다.요청당 디스크 장치 대기 시간. 즉, 사용량이 많은 디스크(긴 요청 큐)나 네트워크를 통해 액세스되는 디스크에서는 이 수치가 더 높을 수 있습니다.

http://be-n.com/spw/you-can-list-a-million-files-in-a-directory-but-not-with-ls.html

대부분의 경우 glibc에서 사용하는 버퍼링 양은 문제를 일으키지 않습니다. 일반적으로 버퍼링된 코드를 우회하는 이유로 이러한 극단적인 경우를 지적하면 "성급한 최적화" 또는 유사한 성가심이 발생할 수 있습니다 :-).

https://github.com/BurntSushi/walkdir/issues/108


Linus의 기사 중 하나를 읽는 데 너무 지치지 않으셨다면, 파일 시스템 디렉토리 미리 가져오기에 대한 몇 가지 의견을 읽어보시기 바랍니다. 그들은 그것을 공개할 수도 있고 공개하지 않을 수도 있습니다.

https://lore.kernel.org/lkml/[이메일 보호됨]/T/#u

ext4_readdir()은 Linus의 호언장담을 만족시키기 위해 변경되지 않았습니다. 나는 또한 그가 다른 파일 시스템의 readdir()에서 사용하기를 원하는 방법을 볼 수 없습니다. 나는 XFS가 디렉토리에 대해 (물리적으로 색인화된) 버퍼 캐시도 사용한다고 생각합니다.핵무기디렉토리가 조각화된 경우 미리 읽기 구현]. bachefs는 readdir()의 페이지 캐시를 전혀 사용하지 않으며 자체 btree 캐시를 사용합니다. btrfs에 뭔가 빠졌을 수도 있습니다.

관련 정보