Skip to content

Commit 5eefff3

Browse files
RudolfWeeberclaude
andcommitted
core: branchless minimum-image fold and flat Verlet-list cutoff table
Wave 1 of the short-range hot-path optimization: - Branchless cuboid minimum-image fold. Encode periodicity into a masked inverse box length (0 for non-periodic directions) so the per-component fold reduces to `dx - rint(dx * inv_masked) * L` with no branch. `rint` maps to a single rounding instruction. Results match the previous round-based fold across the pair-loop input domain (separations below 1.5 box lengths); confirmed bitwise-identical on the canonical lj and p3m trajectories. - Flat per-type-pair squared-cutoff table in VerletCriterion. The per-candidate cutoff query in the Verlet-list build becomes a dense table load instead of walking the InteractionsNonBonded pointer table; inactive pairs store a negative sentinel so the distance comparison rejects them without a separate activity check. - Hoist cuboid box parameters by value into the Verlet-build kernels via CuboidMinimumImage, instead of chasing the BoxGeometry reference for the box lengths on every candidate pair. - Force-inline Utils::Vector operator+= / operator-= (previously outlined as .isra clones called from inside the pair kernel). Canonical identity bitwise-preserved (lj, p3m); unit tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3a54ec1 commit 5eefff3

5 files changed

Lines changed: 169 additions & 34 deletions

File tree

src/core/BoxGeometry.hpp

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,25 @@
3535
namespace detail {
3636
/**
3737
* @brief Get the minimum-image distance between two coordinates.
38-
* @param a Coordinate of the terminal point.
39-
* @param b Coordinate of the initial point.
40-
* @param box_length Box length.
41-
* @param box_length_inv Inverse box length
42-
* @param box_length_half Half box length
43-
* @param periodic Box periodicity.
38+
*
39+
* Branchless fold: the periodicity is encoded in the masked inverse box
40+
* length (0 for non-periodic directions, where <tt>rint</tt> then yields a
41+
* zero image shift). Uses <tt>rint</tt> (round half to even) rather than
42+
* <tt>round</tt>, so it maps to a single rounding instruction; the two only
43+
* differ for separations of exactly half a box length, where both images
44+
* are equidistant.
45+
*
46+
* @param a Coordinate of the terminal point.
47+
* @param b Coordinate of the initial point.
48+
* @param box_length Box length.
49+
* @param box_length_inv_masked Inverse box length if periodic, 0 otherwise.
4450
* @return Shortest distance from @p b to @p a across periodic images,
4551
* i.e. <tt>a - b</tt>. Can be negative.
4652
*/
4753
template <typename T>
48-
T get_mi_coord(T a, T b, T box_length, T box_length_inv, T box_length_half,
49-
bool periodic) {
54+
T get_mi_coord_masked(T a, T b, T box_length, T box_length_inv_masked) {
5055
auto const dx = a - b;
51-
52-
if (periodic && (std::abs(dx) > box_length_half)) {
53-
return dx - std::round(dx * box_length_inv) * box_length;
54-
}
55-
56-
return dx;
56+
return dx - std::rint(dx * box_length_inv_masked) * box_length;
5757
}
5858

5959
/**
@@ -66,8 +66,8 @@ T get_mi_coord(T a, T b, T box_length, T box_length_inv, T box_length_half,
6666
* i.e. <tt>a - b</tt>. Can be negative.
6767
*/
6868
template <typename T> T get_mi_coord(T a, T b, T box_length, bool periodic) {
69-
return get_mi_coord(a, b, box_length, 1. / box_length, 0.5 * box_length,
70-
periodic);
69+
return get_mi_coord_masked(a, b, box_length,
70+
periodic ? T{1.} / box_length : T{0.});
7171
}
7272

7373
/** @brief Calculate image box shift vector.
@@ -95,6 +95,37 @@ inline auto unfolded_position(Utils::Vector3d const &pos,
9595

9696
enum class BoxType { CUBOID = 0, LEES_EDWARDS = 1 };
9797

98+
/**
99+
* @brief Cuboid minimum-image fold parameters for hot pair loops.
100+
*
101+
* Capture an instance by value in a kernel to hoist the box data out of the
102+
* pair loop: member loads then come from the kernel's own frame and the
103+
* compiler can keep them in registers, instead of re-reading them through a
104+
* @ref BoxGeometry reference for every pair. Only valid for cuboid boxes;
105+
* Lees-Edwards boxes need the full @ref BoxGeometry::get_mi_vector.
106+
*/
107+
class CuboidMinimumImage {
108+
Utils::Vector3d m_length;
109+
Utils::Vector3d m_length_inv_masked;
110+
111+
public:
112+
CuboidMinimumImage(Utils::Vector3d const &length,
113+
Utils::Vector3d const &length_inv_masked)
114+
: m_length(length), m_length_inv_masked(length_inv_masked) {}
115+
116+
/** @brief Squared minimum-image distance between two coordinates. */
117+
ESPRESSO_ATTR_ALWAYS_INLINE inline double
118+
dist2(Utils::Vector3d const &a, Utils::Vector3d const &b) const {
119+
double acc = 0.;
120+
for (auto c = 0u; c < 3u; ++c) {
121+
auto const dx = detail::get_mi_coord_masked(a[c], b[c], m_length[c],
122+
m_length_inv_masked[c]);
123+
acc += dx * dx;
124+
}
125+
return acc;
126+
}
127+
};
128+
98129
class BoxGeometry {
99130
public:
100131
BoxGeometry() {
@@ -121,6 +152,10 @@ class BoxGeometry {
121152
Utils::Vector3d m_length = {1., 1., 1.};
122153
/** Inverse side lengths of the box */
123154
Utils::Vector3d m_length_inv = {1., 1., 1.};
155+
/** Inverse side lengths for periodic directions, 0 for non-periodic ones.
156+
* Folding the periodicity into the inverse length makes the cuboid
157+
* minimum-image fold branchless (see @ref detail::get_mi_coord_masked). */
158+
Utils::Vector3d m_length_inv_masked = {1., 1., 1.};
124159
/** Half side lengths of the box */
125160
Utils::Vector3d m_length_half = {0.5, 0.5, 0.5};
126161

@@ -134,7 +169,10 @@ class BoxGeometry {
134169
* @param coord The coordinate to set the periodicity for.
135170
* @param val True if this direction should be periodic.
136171
*/
137-
void set_periodic(unsigned coord, bool val) { m_periodic.set(coord, val); }
172+
void set_periodic(unsigned coord, bool val) {
173+
m_periodic.set(coord, val);
174+
m_length_inv_masked[coord] = val ? m_length_inv[coord] : 0.;
175+
}
138176

139177
/**
140178
* @brief Check periodicity in direction.
@@ -173,6 +211,9 @@ class BoxGeometry {
173211
assert(box_l > Utils::Vector3d::broadcast(0.));
174212
m_length = box_l;
175213
m_length_inv = {1. / box_l[0], 1. / box_l[1], 1. / box_l[2]};
214+
for (auto c = 0u; c < 3u; ++c) {
215+
m_length_inv_masked[c] = m_periodic[c] ? m_length_inv[c] : 0.;
216+
}
176217
m_length_half = 0.5 * box_l;
177218
}
178219

@@ -193,8 +234,14 @@ class BoxGeometry {
193234
template <typename T> T inline get_mi_coord(T a, T b, unsigned coord) const {
194235
assert(coord <= 2u);
195236

196-
return detail::get_mi_coord(a, b, m_length[coord], m_length_inv[coord],
197-
m_length_half[coord], m_periodic[coord]);
237+
return detail::get_mi_coord_masked(
238+
a, b, static_cast<T>(m_length[coord]),
239+
static_cast<T>(m_length_inv_masked[coord]));
240+
}
241+
242+
/** @brief Cuboid minimum-image fold parameters for hoisting into kernels. */
243+
auto cuboid_minimum_image() const {
244+
return CuboidMinimumImage{m_length, m_length_inv_masked};
198245
}
199246

200247
/**

src/core/nonbonded_interactions/VerletCriterion.hpp

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
#include <utils/index.hpp>
3131
#include <utils/math/sqr.hpp>
3232

33+
#include <algorithm>
34+
#include <cassert>
35+
#include <cstddef>
36+
#include <vector>
37+
3338
struct GetNonbondedCutoff {
3439
GetNonbondedCutoff(System::System const &system) : m_system{system} {}
3540
auto operator()(int type_i, int type_j) const {
@@ -54,7 +59,14 @@ template <typename CutoffGetter = GetNonbondedCutoff> class VerletCriterion {
5459
return inactive_cutoff;
5560
return Utils::sqr(x + m_skin);
5661
}
57-
CutoffGetter get_nonbonded_cutoff;
62+
/** Dense row-major table of squared effective (cutoff + skin) values per
63+
* type pair, @ref inactive_cutoff for inactive pairs. The per-type-pair
64+
* cutoff query runs once per candidate pair in the Verlet-list build, so
65+
* it must be a plain load instead of a walk through the
66+
* @ref InteractionsNonBonded pointer table.
67+
*/
68+
std::vector<double> m_eff_cut2_table;
69+
int m_n_types;
5870

5971
public:
6072
VerletCriterion(System::System const &system, double skin, double max_cut,
@@ -63,8 +75,27 @@ template <typename CutoffGetter = GetNonbondedCutoff> class VerletCriterion {
6375
: m_skin(skin), m_eff_max_cut2(eff_cutoff_sqr(max_cut)),
6476
m_eff_coulomb_cut2(eff_cutoff_sqr(coulomb_cut)),
6577
m_eff_dipolar_cut2(eff_cutoff_sqr(dipolar_cut)),
66-
m_collision_cut2(eff_cutoff_sqr(collision_detection_cutoff)),
67-
get_nonbonded_cutoff(system) {}
78+
m_collision_cut2(eff_cutoff_sqr(collision_detection_cutoff)) {
79+
CutoffGetter const get_nonbonded_cutoff(system);
80+
auto const max_type = system.nonbonded_ias->get_max_seen_particle_type();
81+
m_n_types = std::max(max_type + 1, 1);
82+
m_eff_cut2_table.assign(static_cast<std::size_t>(m_n_types) *
83+
static_cast<std::size_t>(m_n_types),
84+
inactive_cutoff);
85+
for (int type_i = 0; type_i <= max_type; ++type_i) {
86+
for (int type_j = type_i; type_j <= max_type; ++type_j) {
87+
auto const cut = get_nonbonded_cutoff(type_i, type_j);
88+
auto const eff_cut2 =
89+
(cut == inactive_cutoff) ? inactive_cutoff : Utils::sqr(cut + skin);
90+
m_eff_cut2_table[static_cast<std::size_t>(type_i) *
91+
static_cast<std::size_t>(m_n_types) +
92+
static_cast<std::size_t>(type_j)] = eff_cut2;
93+
m_eff_cut2_table[static_cast<std::size_t>(type_j) *
94+
static_cast<std::size_t>(m_n_types) +
95+
static_cast<std::size_t>(type_i)] = eff_cut2;
96+
}
97+
}
98+
}
6899

69100
bool operator()(const Particle &p1, const Particle &p2, double dist2) const {
70101
if (dist2 > m_eff_max_cut2)
@@ -88,9 +119,15 @@ template <typename CutoffGetter = GetNonbondedCutoff> class VerletCriterion {
88119
return true;
89120
#endif
90121

91-
// Within short-range distance (including dpd and the like)
92-
auto const ia_cut = get_nonbonded_cutoff(p1.type(), p2.type());
93-
return (ia_cut != inactive_cutoff) &&
94-
(dist2 <= Utils::sqr(ia_cut + m_skin));
122+
// Within short-range distance (including dpd and the like). Inactive
123+
// pairs hold inactive_cutoff (negative) in the table, so the comparison
124+
// rejects them without a separate activity check.
125+
auto const type_i = p1.type();
126+
auto const type_j = p2.type();
127+
assert(type_i >= 0 and type_i < m_n_types);
128+
assert(type_j >= 0 and type_j < m_n_types);
129+
return dist2 <= m_eff_cut2_table[static_cast<std::size_t>(type_i) *
130+
static_cast<std::size_t>(m_n_types) +
131+
static_cast<std::size_t>(type_j)];
95132
}
96133
};

src/core/short_range_cabana.hpp

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,27 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
121121
}
122122
}
123123

124+
// Hoist the cuboid minimum-image parameters by value: the fold runs once
125+
// per candidate pair and must not chase the BoxGeometry reference for its
126+
// box lengths every time. Lees-Edwards boxes take the full BoxGeometry
127+
// path (shear offset handling).
128+
bool const has_lees_edwards = box_geo.type() == BoxType::LEES_EDWARDS;
129+
auto const cuboid_minimum_image = box_geo.cuboid_minimum_image();
130+
auto const minimum_image_dist2 =
131+
[&box_geo, has_lees_edwards, cuboid_minimum_image](
132+
Utils::Vector3d const &a, Utils::Vector3d const &b) {
133+
return has_lees_edwards ? box_geo.get_mi_dist2(a, b)
134+
: cuboid_minimum_image.dist2(a, b);
135+
};
136+
124137
// Iterate the cells' store-ROW bags directly and REBIND two cached views
125138
// (p1 + partner) per work item via Particle::attach_to_store, instead of
126139
// driving RowParticleRange iterators (each embeds a Particle by value, so
127140
// std::next(it) and per-neighbour range begin()/end() would build fresh
128141
// Particles). One reused view per role per work item (one cell per Kokkos
129142
// work item) is thread-safe. Iteration ORDER is unchanged.
130-
auto intra_kernel = [&cells, &box_geo, &verlet_criterion, &id_to_index,
131-
&intra_operator, &interleaved_positions,
143+
auto intra_kernel = [&cells, minimum_image_dist2, &verlet_criterion,
144+
&id_to_index, &intra_operator, &interleaved_positions,
132145
max_id](const int i) {
133146
auto &store = cells[i]->store();
134147
// Contiguous store-row range; clean store, so index directly.
@@ -183,7 +196,7 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
183196
Utils::Vector3d{p2_base[0u], p2_base[pos_comp_stride],
184197
p2_base[2u * pos_comp_stride]};
185198
if (verlet_criterion(p1, p2,
186-
box_geo.get_mi_dist2(p1_pos, p2_pos))) {
199+
minimum_image_dist2(p1_pos, p2_pos))) {
187200
auto const jj = id_to_index(id_column[row_b]);
188201
if (jj >= 0) {
189202
intra_operator(ii, jj);
@@ -196,8 +209,8 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
196209
}
197210
};
198211

199-
auto inter_kernel = [&cells, &box_geo, &verlet_criterion, &id_to_index,
200-
&inter_operator, &interleaved_positions,
212+
auto inter_kernel = [&cells, minimum_image_dist2, &verlet_criterion,
213+
&id_to_index, &inter_operator, &interleaved_positions,
201214
max_id](const int i) {
202215
auto &store = cells[i]->store();
203216
// Contiguous store-row range; clean store, so index directly.
@@ -247,7 +260,7 @@ link_cell_kokkos(std::span<Cell *const> cells, BoxGeometry const &box_geo,
247260
Utils::Vector3d{p2_base[0u], p2_base[pos_comp_stride],
248261
p2_base[2u * pos_comp_stride]};
249262
if (verlet_criterion(p1, p2,
250-
box_geo.get_mi_dist2(p1_pos, p2_pos))) {
263+
minimum_image_dist2(p1_pos, p2_pos))) {
251264
auto const jj = id_to_index(id_column[row_k]);
252265
if (jj >= 0) {
253266
inter_operator(ii, jj);

src/utils/include/utils/Vector.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <boost/qvm/vec_traits.hpp>
3131

3232
#include "utils/Array.hpp"
33+
#include "utils/attributes.hpp"
3334

3435
#include <algorithm>
3536
#include <cassert>
@@ -270,7 +271,8 @@ auto operator+(Vector<T, N> const &a, Vector<U, N> const &b) {
270271
}
271272

272273
template <std::size_t N, typename T>
273-
auto &operator+=(Vector<T, N> &a, Vector<T, N> const &b) {
274+
ESPRESSO_ATTR_ALWAYS_INLINE inline auto &operator+=(Vector<T, N> &a,
275+
Vector<T, N> const &b) {
274276
std::ranges::transform(a, b, std::begin(a), std::plus<T>());
275277
return a;
276278
}
@@ -288,7 +290,8 @@ Vector<T, N> operator-(Vector<T, N> const &a) {
288290
}
289291

290292
template <std::size_t N, typename T>
291-
Vector<T, N> &operator-=(Vector<T, N> &a, Vector<T, N> const &b) {
293+
ESPRESSO_ATTR_ALWAYS_INLINE inline Vector<T, N> &
294+
operator-=(Vector<T, N> &a, Vector<T, N> const &b) {
292295
std::ranges::transform(a, b, std::begin(a), std::minus<T>());
293296
return a;
294297
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (C) 2010-2026 The ESPResSo project
3+
*
4+
* This file is part of ESPResSo.
5+
*
6+
* ESPResSo is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* ESPResSo is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
#pragma once
21+
22+
/** \file
23+
* Compiler-attribute macros shared across utils headers.
24+
*
25+
* Uses the same macro name and guard as the core attributes header, so
26+
* either header can be included first without redefinition.
27+
*/
28+
29+
#ifndef ESPRESSO_ATTR_ALWAYS_INLINE
30+
#if defined(__GNUG__) or defined(__clang__)
31+
#define ESPRESSO_ATTR_ALWAYS_INLINE [[gnu::always_inline]]
32+
#else
33+
#define ESPRESSO_ATTR_ALWAYS_INLINE
34+
#endif
35+
#endif

0 commit comments

Comments
 (0)