Question: ByteBuffer vs MemorySegment for binary (de)serializiation and in-memory buffer pool
Johannes Lichtenberger
lichtenberger.johannes at gmail.com
Sat Sep 3 23:17:54 UTC 2022
I think you should probably use MemorySegment::mapFile and directly obtain
a MemorySegment from that call. I'd try to create a segment for the whole
file and get pageSize fragments at specified offsets from this file memory
segment. I think otherwise with your current approach you're calling mmap
and munmap way too often as it involves costly system calls and mappings to
virtual addresses (the contents should also be paged in on demand, not at
once). Also I think, that even multiple calls to the `mapFile` method and
the same file + start address and offset should not add up virtual memory
if it's a shared mapping (MAP_SHARED and I think it is).
As far as I can tell the approach works in SirixDB so far, but as I want to
cache some pages in memory (your buffer manager pendant -- on-heap
currently) which in my case have to be reconstructed from scattered page
fragments probably it doesn't seem to be faster nor slower than a pure
FileChannel based approach. Usually with memory mapping I think you'd
rather not want to have an application cache as it's already available
off-heap (that said if you store these page sized memory segment views in
the buffer manager it should work IMHO).
Hope any stuff which I mentioned if it's not correct will be corrected by
more experienced engineers.
Kind regards
Johannes
Gavin Ray <ray.gavin97 at gmail.com> schrieb am So., 4. Sep. 2022, 00:26:
> Radosław, I tried to implement your advice but I think I might have
> implemented it incorrectly
> With JMH, I get very poor results:
>
> Benchmark Mode Cnt
> Score Error Units
> DiskManagerBenchmarks._01_01_writePageDiskManager thrpt 4
> 552595.393 � 77869.814 ops/s
> DiskManagerBenchmarks._01_02_writePageMappedDiskManager thrpt 4
> 174.588 � 111.846 ops/s
> DiskManagerBenchmarks._02_01_readPageDiskManager thrpt 4
> 640469.183 � 104851.381 ops/s
> DiskManagerBenchmarks._02_02_readPageMappedDiskManager thrpt 4
> 133564.674 � 10693.985 ops/s
>
> The difference in writing is ~550,000 vs 174(!)
> In reading it is ~640,000 vs ~130,000
>
> This is the implementation code:
>
> public void readPage(PageId pageId, MemorySegment pageBuffer) {
> int pageOffset = pageId.value() * Constants.PAGE_SIZE;
> MemorySegment mappedBuffer =
> raf.getChannel().map(FileChannel.MapMode.READ_WRITE, pageOffset,
> Constants.PAGE_SIZE, session);
> mappedBuffer.load();
> pageBuffer.copyFrom(mappedBuffer);
> mappedBuffer.unload();
> }
>
> public void writePage(PageId pageId, MemorySegment pageBuffer) {
> int pageOffset = pageId.value() * Constants.PAGE_SIZE;
> MemorySegment mappedBuffer =
> raf.getChannel().map(FileChannel.MapMode.READ_WRITE, pageOffset,
> Constants.PAGE_SIZE, session);
> mappedBuffer.copyFrom(pageBuffer);
> mappedBuffer.force();
> mappedBuffer.unload();
> }
>
> Am I doing something wrong here (I think I probably am)
>
> On Fri, Sep 2, 2022 at 1:16 PM Gavin Ray <ray.gavin97 at gmail.com> wrote:
>
>> Thank you very much for the advice, I will implement these suggestions =)
>>
>> On Fri, Sep 2, 2022 at 12:12 PM Radosław Smogura <mail at smogura.eu> wrote:
>>
>>> Hi Gavin,
>>>
>>>
>>>
>>> I see you do a good progress.
>>>
>>>
>>>
>>> This is good approach. Minor improvement would be to use
>>> MemorySegment.ofBuffer(), to create memory segment from _*direct*_ byte
>>> buffer. This way you would have consistency (using only MemorySegment) and
>>> FileChannels or other methods to manage file size.
>>>
>>>
>>>
>>> Most probably you would like to use MappedByteBuffer.force() to flush
>>> changes to disk (equivalent of sync in Linux) – i.e. to be sure transaction
>>> is persisted or for write ahead log.
>>>
>>>
>>>
>>> In most cases if you want to work with zero-copy reads, you have to map
>>> a whole file as direct buffer / memory segment. You would need to enlarge
>>> file using (most probably file channel) or other methods, if you want to
>>> append new data (otherwise sigbus or segfault can be generated – can result
>>> in exception or crash).
>>>
>>>
>>>
>>> You can compare different approaches using JMH to measure reads and
>>> writes performance.
>>>
>>>
>>>
>>> Kind regards,
>>>
>>> Rado Smogura
>>>
>>>
>>>
>>> *From: *Gavin Ray <ray.gavin97 at gmail.com>
>>> *Sent: *Friday, September 2, 2022 5:50 PM
>>> *To: *Johannes Lichtenberger <lichtenberger.johannes at gmail.com>
>>> *Cc: *Maurizio Cimadamore <maurizio.cimadamore at oracle.com>;
>>> panama-dev at openjdk.org
>>> *Subject: *Re: Question: ByteBuffer vs MemorySegment for binary
>>> (de)serializiation and in-memory buffer pool
>>>
>>>
>>>
>>> On a related note, is there any way to do zero-copy reads from files
>>> using MemorySegments for non-Memory-Mapped files?
>>>
>>>
>>>
>>> Currently I'm using "SeekableByteChannel" and wrapping the MemorySegment
>>> using ".asByteBuffer()"
>>>
>>> Is this the most performant way?
>>>
>>>
>>>
>>> ========================
>>>
>>>
>>>
>>> class DiskManager {
>>>
>>> private final RandomAccessFile raf;
>>>
>>> private final SeekableByteChannel dbFileChannel;
>>>
>>>
>>>
>>> public void readPage(PageId pageId, MemorySegment pageBuffer) {
>>>
>>> int pageOffset = pageId.value() * Constants.PAGE_SIZE;
>>>
>>> dbFileChannel.position(pageOffset);
>>>
>>> dbFileChannel.read(pageBuffer.asByteBuffer());
>>>
>>> }
>>>
>>>
>>>
>>> public void writePage(PageId pageId, MemorySegment pageBuffer) {
>>>
>>> int pageOffset = pageId.value() * Constants.PAGE_SIZE;
>>>
>>> dbFileChannel.position(pageOffset);
>>>
>>> dbFileChannel.write(pageBuffer.asByteBuffer());
>>>
>>> }
>>>
>>> }
>>>
>>>
>>>
>>> On Thu, Sep 1, 2022 at 6:13 PM Johannes Lichtenberger <
>>> lichtenberger.johannes at gmail.com> wrote:
>>>
>>> I think it's a really good idea to use off-heap memory for the Buffer
>>> Manager/the pages with the stored records. In my case, I'm working on an
>>> immutable, persistent DBMS currently storing JSON and XML with only one
>>> read-write trx per resource concurrently and if desired in parallel to N
>>> read-only trx bound to specific revisions (in the relational world the term
>>> for a resource is a relation/table). During an import of a close to 4Gb
>>> JSON file with intermediate commits, I found out that depending on the
>>> number of records/nodes accumulated in the trx intent log (a trx private
>>> map more or less), after which a commit and thus a sync to disk with
>>> removing the pages from the log is issued, the GC runs are >= 100ms most of
>>> the times and the objects are long-lived and are promoted to the old gen
>>> obviously, which seems to take these >= 100ms. That is I'll have to study
>>> how Shenandoah works, but in this case, it brings no advantage regarding
>>> the latency.
>>>
>>>
>>>
>>> Maybe it would make sense to store the data in the record instances also
>>> off-head, as Gavin did with his simple Buffer Manager :-) that said
>>> lowering the max records number after which to commit and sync to disk also
>>> has a tremendous effect and with Shenandoah, the GC times are less than a
>>> few ms at least.
>>>
>>>
>>>
>>> I'm using the Foreign Memory API however already to store the data in
>>> memory-mapped files, once the pages (or page fragments) and records therein
>>> are serialized and then written to the memory segment after compression and
>>> hopefully soon encyrption.
>>>
>>>
>>>
>>> Kind regards
>>>
>>> Johannes
>>>
>>>
>>>
>>>
>>>
>>>
>>>
>>> Am Do., 1. Sept. 2022 um 22:52 Uhr schrieb Maurizio Cimadamore <
>>> maurizio.cimadamore at oracle.com>:
>>>
>>>
>>> On 01/09/2022 19:26, Gavin Ray wrote:
>>> > I think this is where my impression of verbosity is coming from, in
>>> > [1] I've linked a gist of ByteBuffer vs MemorySegment implementation
>>> > of a page header struct,
>>> > and it's the layout/varhandles that are the only difference, really.
>>> >
>>> Ok, I see what you mean, of course; thanks for the Gist.
>>>
>>> In this case I think the instance accessor we added on MemorySegment
>>> will bring the code more or less to the same shape as what it used to be
>>> with the ByteBuffer API.
>>>
>>> Using var handles is very useful when you want to access elements (e.g.
>>> structs inside other structs inside arrays) as it takes all the offset
>>> computation out of the way.
>>>
>>> If you're happy enough with hardwired offsets (and I agree that in this
>>> case things might be good enough), then there's nothing wrong with using
>>> the ready-made accessor methods.
>>>
>>> Maurizio
>>>
>>>
>>>
>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/panama-dev/attachments/20220904/afea44f6/attachment-0001.htm>
More information about the panama-dev
mailing list