Skip to content

Calculate Calendar.ISO.quarter_of_year/3 with bitwise arithmetic - #15945

Closed
preciz wants to merge 1 commit into
elixir-lang:mainfrom
preciz:optimize-calendar-iso-quarter
Closed

preciz wants to merge 1 commit into
elixir-lang:mainfrom
preciz:optimize-calendar-iso-quarter

Conversation

@preciz

@preciz preciz commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Assisted-by: Antigravity:Gemini 3.8 Flash

Minor speedup.

On the Erlang/OTP 29 JIT (BeamAsm), constant integer division div(..., 3) emits the hardware idiv instruction, which has high latency (10–35 CPU cycles) and cannot be pipelined. The bitwise equivalent replaces division with a fast pipelined multiply (imul, ~3 cycles) and arithmetic shift right (sar, 1 cycle), avoiding idiv entirely while generating smaller native code.

Bench:

# Run with: ./bin/elixir bench.exs
Mix.install([:benchee])

defmodule Baseline do
  defguardp is_year(year) when is_integer(year)
  defguardp is_month(month) when month in 1..12
  defguardp is_day(day) when is_integer(day)

  def quarter_of_year(year, month, day)
      when is_year(year) and is_month(month) and is_day(day) do
    div(month - 1, 3) + 1
  end
end

defmodule Proposed do
  import Bitwise

  defguardp is_year(year) when is_integer(year)
  defguardp is_month(month) when month in 1..12
  defguardp is_day(day) when is_integer(day)

  def quarter_of_year(year, month, day)
      when is_year(year) and is_month(month) and is_day(day) do
    (month * 5 + 14) >>> 4
  end
end

defmodule Bench do
  def baseline({year, month, day}), do: Baseline.quarter_of_year(year, month, day)
  def proposed({year, month, day}), do: Proposed.quarter_of_year(year, month, day)
end

inputs = %{
  "Q1 (January)" => {2026, 1, 15},
  "Q2 (April)" => {2026, 4, 15},
  "Q3 (July)" => {2026, 7, 15},
  "Q4 (December)" => {2026, 12, 15}
}

Benchee.run(
  %{
    "main" => &Bench.baseline/1,
    "HEAD" => &Bench.proposed/1
  },
  inputs: inputs,
  time: 2,
  warmup: 1,
  memory_time: 1
)

Results:

Operating System: Linux
CPU Information: AMD Ryzen 7 8845HS
Elixir 1.21.0-dev / Erlang 29.0.5 (JIT enabled)

##### With input Q1 (January) #####
Name           ips        average  deviation         median         99th %
HEAD       32.83 M       30.46 ns   ±195.63%          30 ns          41 ns
main       31.27 M       31.98 ns    ±98.91%          30 ns          41 ns

Comparison: 
HEAD       32.83 M
main       31.27 M - 1.05x slower +1.52 ns

##### With input Q2 (April) #####
Name           ips        average  deviation         median         99th %
HEAD       32.33 M       30.93 ns   ±129.28%          30 ns          41 ns
main       31.45 M       31.80 ns   ±134.16%          30 ns          41 ns

Comparison: 
HEAD       32.33 M
main       31.45 M - 1.03x slower +0.87 ns

##### With input Q3 (July) #####
Name           ips        average  deviation         median         99th %
HEAD       32.55 M       30.72 ns   ±104.32%          30 ns          41 ns
main       31.81 M       31.44 ns    ±84.96%          30 ns          41 ns

Comparison: 
HEAD       32.55 M
main       31.81 M - 1.02x slower +0.72 ns

##### With input Q4 (December) #####
Name           ips        average  deviation         median         99th %
HEAD       32.58 M       30.69 ns   ±180.00%          30 ns          40 ns
main       31.95 M       31.30 ns   ±183.60%          30 ns          41 ns

Comparison: 
HEAD       32.58 M
main       31.95 M - 1.02x slower +0.61 ns

Replace `div(month - 1, 3) + 1` with `(month * 5 + 14) >>> 4` for months 1..12 to avoid hardware integer division.

Assisted-by: Antigravity:Gemini 3.8 Flash
@sabiwara

Copy link
Copy Markdown
Contributor

I'm a bit concerned we're over-optimizing here, the win is quite small and the code is more cryptic.

@preciz

preciz commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor Author

I'm a bit concerned we're over-optimizing here, the win is quite small and the code is more cryptic.

On one hand I agree on the other hand I think the function itself is quite self contained and I believe that in the age of large language models it is easy to understand it once somebody wants to.

In this module we already went into this direction however I am hundred percent okay with closing this pull request.

@josevalim

Copy link
Copy Markdown
Member

Yeah, for 1.05, not worth it :)Thank you!

@josevalim josevalim closed this Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants