nathanl

nathanl

“Programming Phoenix” includes this statement:

“Templates are precompiled. Phoenix doesn’t need to copy strings for each rendered template. At the hardware level, you’ll see caching come into play for these strings where it never did before.”

This sounds awesome, but I want to understand how it works. Precompiled templates are definitely more memory-efficient, but so far, I’ve been unable to see a caching effect in action. I had a theory:

  • If the same string is used for (eg) the header on each request, it will be found at the same location in memory (if it’s a refc binary)
  • The completed template will be given to Cowboy as an iolist, and Cowboy will call writev on the socket with some of the same memory addresses each time
  • The fact that writev gets some of the same memory addresses for each request will signal the operating system to look for those strings in CPU cache

I’ve written a small Phoenix app to test this theory, but so far it seems to be wrong. (There’s a detailed writeup in the README.)

Can anyone point me to a way that Phoenix benefits from hardware caching, or tell me what I’m doing wrong in my experiment?

(I hope to discuss this in my ElixirConf Talk with @JEG2.)

Showing Posts 1 to 10

nathanl

nathanl OP

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Hardware caching could simply mean that the iodata is generally laid out in contiguous memory, meaning that the L3 and other CPU caches will readily have that information on hand because the COU generally pre fetches chunks of contiguous RAM when it accesses an address.

this would be distinct from an object graph way of representing the template information which may contain pointers all over the place.

In this interpretation of hardware caching it’s simply a property that makes any given request fast, it isn’t about caching across requests necessarily.

OvermindDL1

OvermindDL1

If you want different caching then you could do something that I do. For example I have a few sub-templates in my main templates that are costly to render (conditionals and large data chunk), I could easily have those stored in ETS for a given input and just clear it out every once in a while (in this case this one chunk is the same for everyone so I’d only need to store it in a cache once and just rebuild it when its data changes). In the general case template rendering to iolists are so fast that it is going to be hard to beat (in the general case a caching system lookup would be slower than running render).

nathanl

nathanl OP

Hardware caching could simply mean that the iodata is generally laid out in contiguous memory, meaning that the L3 and other CPU caches will readily have that information on hand because the COU generally pre fetches chunks of contiguous RAM when it accesses an address.

Interesting. I’ll have to see if I can tell whether the addresses given to writev are contiguous in memory.

It’s complicated by the fact that the size of the specific strings changes how the BEAM treats them. They have to be bigger than 512 bytes (in my testing) to both 1) be refcounted, not copied across processes (64 byte minimum) and 2) not be combined when calling writev (based on “ERL_ONHEAP_BIN_LIMIT”, from what I’ve read)

nathanl

nathanl OP

Maybe I’m just Doing It Wrong™ :slight_smile:, but I can’t tell that these addresses are contiguous in memory. Some of the addresses I see using dtrace on a Phoenix app:

(0 bytes): 0x0000000000000000
(0 bytes): 0x00000000b0ac4478
(104 bytes): 0x0000000019f43cae
(106 bytes): 0x0000000019f80d48
(1140 bytes): 0x000000001a003088
(125 bytes): 0x0000000019f45797
(125 bytes): 0x0000000019f45817
(125 bytes): 0x0000000019f45897
(17 bytes): 0x0000000019f420e8
(22 bytes): 0x0000000019f43c98
(22 bytes): 0x0000000019f80af8
(241 bytes): 0x0000000019f45917
(29 bytes): 0x0000000019ec02d0
(3 bytes): 0x0000000019f45794
(3 bytes): 0x0000000019f45814
(3 bytes): 0x0000000019f45894
(35 bytes): 0x0000000019f80b38
(460 bytes): 0x0000000019f455c8
(666 bytes): 0x000000001a003528

I figured that if they’re contiguous, I should be able to take one address, add the number of bytes written from there, and find the start of another address in the list. Eg:

“19f45814” |> Integer.parse(16) |> elem(0) |> Kernel.+(3 * 8) |> Integer.to_string(16)

I don’t find anything that way, but he fact that the first couple are listed as writing “0 bytes” probably means the bytes are rounded, so maybe this is the wrong way to go about it.

asierguti

asierguti

It’s a bit more complex. There is a cache hierarchy, L1, L2, L3, and then you have the main memory. In between the CPU and the main memory you have the TLB coprocesor in the CPU to translate virtual addresses to physical addresses.

L1 memory is very small, and there are 2 caches, one for instructions and one for data.

As benwilson512 mentioned, when there is a cache miss, the CPU will ask data to the next cache level, up to the HDD, if it needs to. When the data is found, it is copied to all the other cache levels, but it’s done in chunks. It is called cache lines.

So, if you want to create a program that is cache friendly, you would do it in this way, creating data that is contiguous, so if there is a cache miss somewhere, you will retrieve as much useful information as possible.

Now, back to phoenix, you may have usual code, or you may have precompiled template code. The difference is that the first one executes at run time, while the second one can execute at compile time and store the results so they ready at run time and there is almost no execution of any of your code.

You should remember how erlang and elixir work. If you pass some data, the data is copied around. This is done to isolate the data from the execution.

If you are really really interested in having full control of your data layout, then I would recommend you to code in C or C++. These are the 2 only high level languages that allow you to specify how the data will sit in the memory, and then use all sort of techniques to speed up execution. For example, you can pass references and pointers, so that there are no copies involved anywhere.

The whole point of elixir and erlang is not to have a language where you can tweak everything to squeeze any CPU cycle. The point is to have a highly concurrent and reliable system.

OvermindDL1

OvermindDL1

Rust I’ve been playing with lately, fantastic C API support, and it has a very good control of memory (though still not to C/C++ extent, however the next version should fix that). This gets me an idea for a mix module to support rust compilation more easily multi-platform than C NIF’s… Hmm…

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe
OvermindDL1

OvermindDL1

Ooo an interop lib, awesome! I may have to play with that…

OvermindDL1

OvermindDL1

Aaaaand it seems unmaintained, does not work with the current version of Erlang/Elixir, is suggesting using the old multirust system instead of the modern rustup (as it in actually requires multirust installed, which cannot happen anymore as multirust removes itself if installed now while saying to migrate to rustup…), etc

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
water
I’m aware of 2 such projects, are there others? git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
itzmidinesh
Hi all, I’m working on a Phoenix project where I need to make external HTTP requests and I’m trying to choose between Req and HTTPoison....
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
leoschweizer
Hi, we run different LLM models across a cluster, with each node loading a configurable subset. Model-specific queues let nodes consume ...
New
csmr
Hi all, I’m new here and would like to hear peoples views on my Elixir/Phoenix based game-engine! :smiley: Mutonex gamedesign was finish...
New
Stef-Verniers
Hello I’ve been coding in Elixir for almost two years now because of my current job where we implemented Elixir in our application. But ...
New

Other Trending Topics Top

dominicletz
Hey there, I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first. ...
New
raa-ziq
Hi everyone :waving_hand: I wanted to share something I’ve been building — NowBlind, a random chat app designed to help people discover ...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews