Google Groepen ondersteunt geen nieuwe Usenet-berichten of -abonnementen meer. Historische content blijft zichtbaar.

mmap oder read?

3 weergaven
Naar het eerste ongelezen bericht

Stefan Reuther

ongelezen,
8 jun 2022, 12:00:5308-06-2022
aan
Hallo,

ich hab hier einen Microservice, der im Wesentlichen den lieben langen
Tag Dateien aus (wenigen) Containerfiles (z.B. *.tar) aufbereitet und
serviert.

Je nachdem, wie ich die Framework-Komponenten zusammenstecke, bekomme
ich für das Lesen einer Datei aus dem Container

mmap, <Verarbeiten>, munmap

oder

malloc, read, <Verarbeiten>, free

Was für Gründe kann es geben, sich bewusst für die eine oder die andere
Art zu entscheiden? Bisher bin ich - wie gesagt, Framework
zusammengesteckt - bei der mmap-Variante. Die hat den Nachteil, nicht
gescheit auf EIO reagieren zu können, was ich erstmal zum Nichtproblem
deklariert hab: wenn die VM anfängt, EIO zu werfen, hab ich ganz andere
Probleme.


Stefan

Rainer Weikusat

ongelezen,
8 jun 2022, 12:27:1708-06-2022
aan
Stefan Reuther <stefa...@arcor.de> writes:
> ich hab hier einen Microservice, der im Wesentlichen den lieben langen
> Tag Dateien aus (wenigen) Containerfiles (z.B. *.tar) aufbereitet und
> serviert.
>
> Je nachdem, wie ich die Framework-Komponenten zusammenstecke, bekomme
> ich für das Lesen einer Datei aus dem Container
>
> mmap, <Verarbeiten>, munmap
>
> oder
>
> malloc, read, <Verarbeiten>, free
>
> Was für Gründe kann es geben, sich bewusst für die eine oder die andere
> Art zu entscheiden?

Für Dateien würde ich im Normalfall mmap benutzen, weil das einfacher zu
handhaben ist: Anstatt Code zu schreiben, der einen Speicherbereich
beschafft und diesen mit Daten füllt, die man verarbeiten möchte, sagt
man einfach dem Kernel «gib mir einen Speicherbereich in dem meine Daten
drinstehen» und kann sie dann direkt verarbeiten.

Wie Hellmut Schellong hier allerdings mal in epischer Breite um die
Jahrtausendwende rum demonstriert hat, ist das normalerweise
langsamer, als read bzw scheint langsamer als read: Wenn man die Daten
erstmal aus einem Kernelpuffer in einen Anwendungspuffer kopiert, lädt
man sie nebenher auch in den Cache und nachfolgende Zugriffe müssen das
nicht noch einmal tun. Nach einem mmap bekommt man hingegen erstmal page
faults und cache und TLB misses, die alle nicht billig sind.

Möglicher Vorteil: Falls das viele Daten sind, auf die man nur lesend
zugreift, und die von anderen Prozessen ebenfalls gebraucht werden,
braucht man nur eine Kopie im Speicher und nicht eine pro Prozeß.

Jakob Hirsch

ongelezen,
9 jun 2022, 06:32:2409-06-2022
aan
On 2022-06-08 17:46, Stefan Reuther wrote:
> Was für Gründe kann es geben, sich bewusst für die eine oder die andere
> Art zu entscheiden? Bisher bin ich - wie gesagt, Framework

TLDR: Wenn man nur sequentiell liest, lohnt sich das wohl nur bei
"großen" Dateien. Die Grenze liegt wohl irgendwo zwischen 1 und 8 MiB,
wie man z.B. unter https://github.com/david-slatinek/c-read-vs.-mmap
sieht (Werte sind bei mir ähnlich, hab allerdings nicht den code
angeschaut).

Das Teure an mmap ist wohl das Einrichten und Aufräumen, also die mmap()
und munmap() calls selbst. Beim Zugriff auf die Pages gibt es u.U. page
faults, der Overhead ist aber m.W. gleich oder ähnlich wie bei einem
syscall (was read() ja ist). Entsprechende mmap()-flags (MAP_HUGETLB,
MAP_POPULATE) und/oder madivse() können das evt. noch verbessern.

Eine recht interessante Diskussion gab es z.B. unter
https://news.ycombinator.com/item?id=19806804

> zusammengesteckt - bei der mmap-Variante. Die hat den Nachteil, nicht
> gescheit auf EIO reagieren zu können, was ich erstmal zum Nichtproblem

Hm, bei truncated files gibt's wohl SIGBUS, ist das bei I/O-Fehler anders?

Bonita Montero

ongelezen,
9 jun 2022, 07:17:4309-06-2022
aan
mmap() hat den Nachteil, dass es recht aufwendig werden kann, I/O-Fehler
abzufangen. Unter Windows ist das alles mit Structured Exception Hand-
ling (ähnlich wie rudimetäre C++-Exceptions, geht aber auch in C) so
simpel wie eine Exception in Java abzufangen, unter Unix wirds mega
-frickelig weil der Code durch den Signal -Handler seinen gewohnten
Pfad verlsssen muss.
Desweiteren ist Memory Mapping gar nicht so effizient, wie man denken
mag, den jede Page macht ihr eigenes Page Fault, während, wenn Du das
File am Stück ins RAM lädst, Du eben nur einen Kernel Call hast. Wenn
Du innerhalb des gemappten Files sogar Random Access Zugriffe machst,
dann greift nichtmal das Prefetching der Platte / SSR, ebensowenig
das Prefetching des Betriebssystems. Da ist es wirklich i.d.R. gravie-
rend effizienter, das File als Ganzes ins RAM zu laden, und dann im
Speicher den Random Access zu machen.
Das sind eigentlich zwei der drei Hauptgründe, weswegen es z.B. kaum
DB-Engines gibt, die mit Memory Mapping arbeiten.

Rainer Weikusat

ongelezen,
9 jun 2022, 10:17:5709-06-2022
aan
Jakob Hirsch <jh.expire...@plonk.de> writes:
> On 2022-06-08 17:46, Stefan Reuther wrote:
>> Was für Gründe kann es geben, sich bewusst für die eine oder die andere
>> Art zu entscheiden? Bisher bin ich - wie gesagt, Framework
>
> TLDR: Wenn man nur sequentiell liest, lohnt sich das wohl nur bei
> "großen" Dateien. Die Grenze liegt wohl irgendwo zwischen 1 und 8 MiB,
> wie man z.B. unter https://github.com/david-slatinek/c-read-vs.-mmap
> sieht (Werte sind bei mir ähnlich, hab allerdings nicht den code
> angeschaut).

Der Sinn von mmap ist zweierlei:

- es ist einfacher zu benutzen, weil man sich Speicherverwaltung
und tatsächliche I/O sparen kann

- es ermöglicht es, Dateien, auf die nur lesend zugegriffen wird
und die von potentiell vielen Prozessen benutzt werden, nur
einmal im Speicher zu haben

Letzteres betrifft vor allem Bibliothek- und Programmdateien. Es ist
kein magischer Performance-Hack. Weil per saldo alles, was man bei
open/malloc/read/close/free in der Anwendung tun müßte, auch so getan
werden muß, nur vom Kernel, ist das auch nicht zu erwarten. Im ganzen
gesehen, ist es aber (aus denselben Gründen) auch nicht 'langsamer',
lediglich fallen die Laufzeitkosten woanders an[*] (und zwar da, wo sie
naive Microbenchmarks betreffen).

[*] Ein Bespiel: Wenn man malloc benutzt, um einen Puffer anzufordern,
treten die Laufzeitkosten dafür, eine entsprechende Anzahl
Speicherseiten in den Addreßraum das Prozesses zu kriegen entweder
während malloc auf oder sie traten zu einem (programmtechnisch gesehen)
längst verganenem Zeitpunk auf. Bei mmap hat man sie beim ersten Zugriff
auf die Daten.

Stefan Reuther

ongelezen,
9 jun 2022, 12:00:5009-06-2022
aan
Am 08.06.2022 um 18:27 schrieb Rainer Weikusat:
> Stefan Reuther <stefa...@arcor.de> writes:
>> Was für Gründe kann es geben, sich bewusst für die eine oder die andere
>> Art zu entscheiden?
>
> Für Dateien würde ich im Normalfall mmap benutzen, weil das einfacher zu
> handhaben ist: Anstatt Code zu schreiben, der einen Speicherbereich
> beschafft und diesen mit Daten füllt, die man verarbeiten möchte, sagt
> man einfach dem Kernel «gib mir einen Speicherbereich in dem meine Daten
> drinstehen» und kann sie dann direkt verarbeiten.

Ungefähr das war der Gedankengang, warum ich spontan auf genau diese
Lösung gekommen war. Es spart das manuelle Anlegen und Verwalten eines
Puffers.


Danke,
Stefan

Stefan Reuther

ongelezen,
9 jun 2022, 12:00:5109-06-2022
aan
Am 09.06.2022 um 12:32 schrieb Jakob Hirsch:
> On 2022-06-08 17:46, Stefan Reuther wrote:
>> Was für Gründe kann es geben, sich bewusst für die eine oder die andere
>> Art zu entscheiden? Bisher bin ich - wie gesagt, Framework
>
> TLDR: Wenn man nur sequentiell liest, lohnt sich das wohl nur bei
> "großen" Dateien. Die Grenze liegt wohl irgendwo zwischen 1 und 8 MiB,
> wie man z.B. unter https://github.com/david-slatinek/c-read-vs.-mmap
> sieht (Werte sind bei mir ähnlich, hab allerdings nicht den code
> angeschaut).
>
> Eine recht interessante Diskussion gab es z.B. unter
> https://news.ycombinator.com/item?id=19806804

Danke für die Links. Für normale, handliche Daten ist also read() aus
Performancegründen zu bevorzugen.

>> zusammengesteckt - bei der mmap-Variante. Die hat den Nachteil, nicht
>> gescheit auf EIO reagieren zu können, was ich erstmal zum Nichtproblem
>
> Hm, bei truncated files gibt's wohl SIGBUS, ist das bei I/O-Fehler anders?

Das gibt genauso SIGBUS, und ist halt kniffliger zu behandeln als 'if
(... == -1)'.


Stefan

Rainer Weikusat

ongelezen,
9 jun 2022, 15:38:0109-06-2022
aan
Stefan Reuther <stefa...@arcor.de> writes:
> Am 09.06.2022 um 12:32 schrieb Jakob Hirsch:
>> On 2022-06-08 17:46, Stefan Reuther wrote:
>>> Was für Gründe kann es geben, sich bewusst für die eine oder die andere
>>> Art zu entscheiden? Bisher bin ich - wie gesagt, Framework
>>
>> TLDR: Wenn man nur sequentiell liest, lohnt sich das wohl nur bei
>> "großen" Dateien. Die Grenze liegt wohl irgendwo zwischen 1 und 8 MiB,
>> wie man z.B. unter https://github.com/david-slatinek/c-read-vs.-mmap
>> sieht (Werte sind bei mir ähnlich, hab allerdings nicht den code
>> angeschaut).
>>
>> Eine recht interessante Diskussion gab es z.B. unter
>> https://news.ycombinator.com/item?id=19806804
>
> Danke für die Links. Für normale, handliche Daten ist also read() aus
> Performancegründen zu bevorzugen.

«Aus Performancegründen» (insofern das eine Rolle spielt) ist zu
bevorzugen, wodurch man größere Lokalität erzielt, weil man dann mehr
von diversem Caching hat, daß hier transparent von der Hardware und auch
von der Virtualpeicherverwaltung gemacht wird[*]. Read mit einem
Anwendungspuffer benutzt normalerweise Speicher, der dem Prozeß bereits
zugeordnet ist. Damit sind die amortisierten Kosten des/ der dafür
notwendigen page faults pro read 0. Ferner sorgt die durch read
vorgenommene Kopie als Seiteneffekt dafür, Nutzdaten in den Cache and
Adreßdaten in die TLB zu bringen (insofern sie nicht bereits
drinstehen).

Allerdings ist der Micro-Benchmark-Ansatz zu simpel: Mit read arbeitet
die CPU trotzdem mehr, dh währenddessen kann niemand anderes die dafür
benutzten CPU-Ausführungseinheiten verwenden. Es verbraucht auch mehr Strom
und erzeugt mehr Abwärme.

[*] Im Grundgenommen ist der virtuelle Addreßraum eines Prozesses ein
teilweise bevölkerter/ gefüllter (populated) Cache für Speicherseiten,
die der Kernel diesem grundsätzlich beliebig zuordnen oder wieder
entziehen kann.

Bonita Montero

ongelezen,
10 jun 2022, 01:30:3010-06-2022
aan
> Allerdings ist der Micro-Benchmark-Ansatz zu simpel: Mit read arbeitet
> die CPU trotzdem mehr, dh währenddessen kann niemand anderes die dafür
> benutzten CPU-Ausführungseinheiten verwenden. ...

Äh, wenn der Kernel auf Ergebnisse vom I/O wartet, dann gibt er die
CPU-Zeit auch für andere Threads wieder frei. Ein read() ist hier
definitiv effizienter.
Hat man Random Access Zugriffe, dann mist man mit mmap() sowieso
Performance-mäßig i.d.R. um Größenordnungen verloren.

Bonita Montero

ongelezen,
10 jun 2022, 03:06:1710-06-2022
aan
Ich hab hier kurz mal ein C++11-Programm geschrieben, was
die niedrige Performance von mmap() beim Random Acess zeigt:

#include <iostream>
#include <cstddef>
#include <random>
#include <atomic>
#include <vector>
#include <system_error>
#include <utility>
#include <chrono>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

using namespace std;
using namespace chrono;

int main( int argc, char **argv )
{
try
{
auto throwSysErr = []( char const *what ) { throw system_error(
(int)errno, system_category(), what ); };
size_t pageSize = sysconf( _SC_PAGESIZE );
if( argc < 2 )
return EXIT_FAILURE;
int file = open( argv[1], 0 );
off_t end = lseek( file, 0, SEEK_END );
if( !end )
return EXIT_FAILURE;
if( end == (off_t)-1 || lseek( file, 0, SEEK_SET ) == (off_t)-1 )
throwSysErr( "can't determine file size" );
size_t nPages = ((size_t)end + pageSize - 1) / pageSize;
atomic_char *block = (atomic_char *)mmap( nullptr, (size_t)end,
PROT_READ, MAP_PRIVATE, file, 0 );
if( !block )
throwSysErr( "can't create file mapping" );
vector<size_t> offsets( nPages );
for( size_t p = 0; p != nPages; ++p )
offsets[p] = p * pageSize;
mt19937_64 mt;
uniform_int_distribution<size_t> uid( 0, nPages - 1 );
for( size_t p = 0; p != nPages; ++p )
swap( offsets[p], offsets[uid( mt )] );
auto start = high_resolution_clock::now();
int lastSec = 0;
constexpr double MB = 1024.0 * 1024.0;
for( size_t iOffset = 0; iOffset != nPages; ++iOffset )
{
(void)block[offsets[iOffset]].load( memory_order_relaxed );
double tDiff = duration_cast<nanoseconds>(
high_resolution_clock::now() - start ).count() / 1.0e9;
if( (int)tDiff != lastSec )
cout << (double)(ptrdiff_t)((iOffset + 1) * pageSize) / tDiff / MB
<< "MB/s" << endl,
lastSec = (int)tDiff;
}
}
catch( exception const &exc )
{
cout << exc.what() << endl;
}
}

Ich habe mal damit ein 4GB RAR-File auf meiner mechanischen 8TB-Platte
lesen wollen und habe das Ganze dann nach 7min abgebrochen - und das
bei einer Platte die linear locker 150MB/s packt.
Dann habe ich eben einen kleinen Algorithmus eingebaut, das die Anzahl
an MB pro Sekunde seit dem Beginn alle Sekunde anzeigt. Da komme ich
dann bei meiner 8TB-Platte im Random Access auf 1,17MB/s - noch Fragen ?

Bonita Montero

ongelezen,
11 jun 2022, 03:42:1111-06-2022
aan
Am 09.06.2022 um 13:18 schrieb Bonita Montero:
> mmap() hat den Nachteil, dass es recht aufwendig werden kann, I/O-Fehler
> abzufangen. Unter Windows ist das alles mit Structured Exception Hand-
> ling (ähnlich wie rudimetäre C++-Exceptions, geht aber auch in C) so
> simpel wie eine Exception in Java abzufangen, unter Unix wirds mega
> -frickelig weil der Code durch den Signal -Handler seinen gewohnten
> Pfad verlsssen muss.

Ich hab das mal Windows-mäßig in C++20 nachprogrammiert und
geschaut, ob sowas wie ein I/O-Error auf einem memory-mapped
File wirklich abzufangen ist.

#include <Windows.h>
#include <iostream>
#include <atomic>

using namespace std;

using XHANDLE = unique_ptr<void, decltype([]( void *h ) { h && h !=
INVALID_HANDLE_VALUE && CloseHandle( (HANDLE)h ); })>;
using XMAP_VIEW = unique_ptr<void, decltype([]( void *p ) { p &&
UnmapViewOfFile( p ); })>;

void readLoop( void *p, size_t n );

int wmain( int argc, wchar_t **argv )
{
if( argc < 2 )
return EXIT_FAILURE;
XHANDLE xhFile( CreateFileW( argv[1], GENERIC_READ, FILE_SHARE_READ |
FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ) );
if( xhFile.get() == INVALID_HANDLE_VALUE )
return EXIT_FAILURE;
LARGE_INTEGER liFileSize;
if( !GetFileSizeEx( xhFile.get(), &liFileSize ) || liFileSize.QuadPart
> (size_t)-1 )
return EXIT_FAILURE;
XHANDLE xhMappging( CreateFileMapping( xhFile.get(), nullptr,
PAGE_READONLY, 0, 0, nullptr ) );
if( !xhMappging.get() )
return EXIT_FAILURE;
XMAP_VIEW mapView( MapViewOfFile( xhMappging.get(), FILE_MAP_READ, 0,
0, 0 ) );
if( !mapView.get() )
return EXIT_FAILURE;
readLoop( mapView.get(), (size_t)liFileSize.QuadPart );
}

void readLoop( void *p, size_t n )
{
atomic_char
*pa = (atomic_char *)p,
*paEnd = pa + n;
__try
{
for( ; ; )
for( atomic_char *paScn = pa; paScn != paEnd; ++paScn )
(void)paScn->load( memory_order_relaxed );
}
__except( EXCEPTION_EXECUTE_HANDLER )
{
cout << "I/O error" << endl;
}
}

Ich hab das zunächst mit einem File auf einer mechanischen Platte
ausprobiert und das File während der unteren Schleife einfach gelöscht.
Das hat nicht funktioniert bzw. das File war dann zwar gelöscht, aber
die Schleife lief weiter, d.h. ich nehme mal an, dass der Kernel die
gemappten Cluster einfach noch offen lässt, während das File in den
Verwaltungsdaten des Dateisystems einfach schon weg war.
Dann habe ich das 4GB-File auf einen Stick kopiert, den raus- und
reingesteckt damit der entsprechend Cache geflusht ist, obiges Pro-
gramm drauf angesetzt und währenddessen den Stick rausgezogen; und
siehe da: ich krieg eine Access Violation und die wird auch sauber
abgefangen.
Sowas wünsche ich mir für Unix bzw. die synchronen Signale wie SIGSEGV,
SIGFPE über einen Signal-Handler zu handeln ist meiner Meinung nach
ziemlicher Murks.

Jan Bruns

ongelezen,
2 jul 2022, 01:27:0802-07-2022
aan

Stefan Reuther:

> ich hab hier einen Microservice, der im Wesentlichen den lieben langen
> Tag Dateien aus (wenigen) Containerfiles (z.B. *.tar) aufbereitet und
> serviert.

Nunja, das klingt aber auch sehr, als wenn da indexlose Komplett-
Datenkomprimierung irgendwie leicht mal auf der Wunschliste auftauchen
könnte.

> Je nachdem, wie ich die Framework-Komponenten zusammenstecke, bekomme
> ich für das Lesen einer Datei aus dem Container
>
> mmap, <Verarbeiten>, munmap
>
> oder
>
> malloc, read, <Verarbeiten>, free
>
> Was für Gründe kann es geben, sich bewusst für die eine oder die andere
> Art zu entscheiden? Bisher bin ich - wie gesagt, Framework
> zusammengesteckt - bei der mmap-Variante. Die hat den Nachteil, nicht
> gescheit auf EIO reagieren zu können, was ich erstmal zum Nichtproblem
> deklariert hab: wenn die VM anfängt, EIO zu werfen, hab ich ganz andere
> Probleme.

Hm. Also jetzt grad hat man ja wohl typischerweise zumindest mal die
Situation, daß man einiges an virtuellem Adressraum frei hat. Und wenn
dann noch die Datei gross im Vgl. zu vorhandenem bzw. für die Anwendung
zu veranschlagendem RAM ist, und das Zugriffsmuster tatsächlich halbwegs
randomös ist, dann hätte man mit einer map immerhin mal dem Kernel
einigermassen präzise beschrieben, was man so braucht.

Aus deiner Problembeschreibung geht aber hervor, daß es akzeptabel ist,
die Datei sogar komplett im RAM zu halten (in der Abwägung taucht ja
überhaupt keine seek Operation auf). Wenn das so egal ist, soll das wohl
auch wirklich einfach so egal sein, schätze ich mal pauschal. Wobei
zugegeben das Experiment von Bonita Montero auf was anderes hinzudeuten
scheint. Vllt. map verwenden, aber dennoch vorab zunächst mal linear
alles zugreifen?

Gruss

Jan Bruns



0 nieuwe berichten