| From: | Greg Burd <greg(at)burd(dot)me> |
|---|---|
| To: | David Geier <geidav(dot)pg(at)gmail(dot)com> |
| Cc: | John Naylor <johncnaylorls(at)gmail(dot)com>, PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org> |
| Subject: | Re: use radix tree for bitmap heap scan |
| Date: | 2026-11-10 19:29:42 |
| Message-ID: | KfXtxghBfJjddmz2RvV9zCwH7nZ1304dA9dkY6UQktA2xSl28Jw433J1vz46bf03WUlhVJp5pOZj-3ioH5jmMjMLhahuI9Uc4nC4avZRVqU=@burd.me |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
On Tuesday, November 10th, 2026 at 11:38 AM, David Geier <geidav(dot)pg(at)gmail(dot)com> wrote:
> Hi Greg!
Hey David. :)
> > The short answer: on its own the growth is a few percent, and the page
> > faults it causes are the rest. The test was a count(*) whose bitmap
> > covers ~17k heap pages, glibc defaults, data cached. Removing most of
> > the growth and its page faults was worth +13.5% at 1 client and +56.8%
> > at 96 clients on a 96-thread host. Under jemalloc, where hardly
> > anything faults, only the rehash and copy of the doubling is left, and
> > removing it was worth +4.9% at 1 client and +6.3% at 96.
>
> Hah, I knew I saw it show up in a production instance. Hadn't realized
> it came from running many queries concurrently. Cool you found that.
Glad I could solve that mystery for you.
> > On Tue, Oct 20, 2026, David Geier wrote:
> >> I'm not entirely sure why it has 0 page faults for successive calls
> >> when I reserve a big hash table. It should still have to fault when
> >> writing to the pages of the initially reserved hash map. It seems to
> >> cache the allocation somehow as I no longer see calls to sbrk().
> >
> > I saw the same. Unpresized, each grow from 256 slots up is a fresh
> > malloc that glibc carves from the top of the heap with brk, and at
> > ExecutorEnd free() trims the heap back with brk, so the next query
> > faults the same pages in again. I counted about 5.7 brk calls and ~730
> > page faults per query. With the table presized there were no brk calls
> > at all, so glibc kept that memory between queries. I haven't worked out
> > from the glibc source why the one big allocation stays and the doubling
> > chain doesn't.
>
> A shot into the dark: sbrk() can only shrink if there's no allocation
> sticking around after the freed memory. Maybe that causes the virtual
> memory to not be returned in some cases?
That's as good a guess as any at this point. I could keep digging, but is
there a point to it now? I do like allocators [1] so it's not like I'm
not interested...
> > Under concurrency those faults are the expensive part: at 96 clients,
> > 27% of all cycles were in native_queued_spin_lock_slowpath under
> > folio_lruvec_lock_irqsave. Presizing removes them only when the
> > estimate is right (4 rows estimated against 17k for "@>" on two int4[]
> > elements, in my test), and under musl, or glibc with a low mmap
> > threshold, it only roughly halved them. So the patch keeps the
> > pagetable memory per backend instead, bounded by work_mem. Details and
> > numbers are in the new thread.
>
> Replying to that part in the other thread.
Saw that, thanks!
> >> Leaving the zeroing to the memory context would have the advantage of
> >> the memory context being able to skip it, if it knows that the memory
> >> it's going to return is already zeroed.
> >
> > I tried that in a prototype last month. Allocating without
> > MCXT_ALLOC_ZERO and setting only each slot's status byte made no
> > difference to the faults (732 vs 732 per query): with 48-byte slots, one
> > status byte per slot touches every page anyway. The memset costs CPU,
> > not faults. calloc() was worse, 1,161 faults per query, which I did not
> > track down.
>
> Yeah, writing just the status byte vs zeroing everything shouldn't make
> much of a difference. The majority of the cost is the page fault. But
> it's curious that calloc() didn't help. Did you remove writing to the
> status byte after changing to calloc()?
I'll have to go back and look, I don't recall off hand. I agree about the
page fault.
> > How this relates to the radix tree: the patch only changes how
> > tidbitmap.c allocates and frees the simplehash arrays and the sorted
> > page list, so the radix tree would replace most of it.
> >
> > The question it might help answer is whether the tree's memory survives
> > from one query to the next. If the nodes come from a memory context
> > that is deleted at ExecutorEnd, whether glibc keeps or returns those
> > blocks depends on their sizes, so it needs measuring. If it does return
> > them, the same approach would carry over. Keep the tree's context per
> > backend, bounded by work_mem, and give it back with a reset callback. I
> > can run the same reproducer against a radix-tree patch once one is ready
> > for testing.
>
> The more important part is that the radix tree grows incrementally while
> the hash map needs to double in size and then be rehashed. That lowers
> the number of page faults considerably.
That is true, and should help (or hide?) the issue.
> --
> David Geier
best.
-greg
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Masahiko Sawada | 2026-11-10 20:45:36 | Re: Crashes on a partition whose concurrent detach never finished |
| Previous Message | Matheus Alcantara | 2026-11-10 19:27:02 | Re: extension_control_path: ignore empty elements and deduplicate directories |