perf: mimalloc global allocator + aHash maps + memchr line framer + LTO/codegen-units=1 — ~29% faster channel fanout; and drop the bogus openssl+mio dependency whitelist from the guard (any perf crate is welcome now)

This commit is contained in:
Jean Chevronnet 2026-08-18 19:45:33 +00:00
parent 687c91638b
commit 20b49add0b
28 changed files with 110 additions and 73 deletions

View file

@ -1,7 +1,7 @@
//! Channels: the `Channel` record, membership, channel modes, bans, invites and
//! JOIN/NAMES.
use std::collections::{HashMap, HashSet};
use crate::map::{HashMap, HashSet};
use crate::module::Hook;
use crate::modules::chathistory::{HistMsg, History};
@ -435,8 +435,8 @@ impl Channel {
Channel {
name: name.to_string(),
topic: None,
members: HashMap::new(),
rmembers: HashMap::new(),
members: HashMap::default(),
rmembers: HashMap::default(),
modes: ChanModes {
no_external: true,
topic_ops: true,
@ -448,14 +448,14 @@ impl Channel {
filters: Vec::new(),
exemptchanops: Vec::new(),
autoop: Vec::new(),
invites: HashSet::new(),
invites: HashSet::default(),
created: now(),
msgflood_hits: HashMap::new(),
msgflood_hits: HashMap::default(),
joinflood_hits: Vec::new(),
joinflood_until: 0,
nickflood_hits: Vec::new(),
nickflood_until: 0,
recent_kicks: HashMap::new(),
recent_kicks: HashMap::default(),
}
}

View file

@ -8,7 +8,7 @@
//! oper = god secret
//! ```
use std::collections::HashMap;
use crate::map::HashMap;
/// Parse a boolean config value (`yes`/`no`/`true`/`false`/`on`/`off`/`1`/`0`).
pub fn yesish(v: &str) -> bool {
@ -117,7 +117,7 @@ impl Default for Config {
dnsbl_reason: "Your host is listed in a DNS blocklist".to_string(),
sasl_server: String::new(),
webirc: Vec::new(),
raw: HashMap::new(),
raw: HashMap::default(),
}
}
}

View file

@ -11,12 +11,12 @@ pub mod core_rehash;
pub mod core_user;
pub mod core_watch;
use std::collections::HashMap;
use crate::map::HashMap;
use crate::command::Command;
pub fn command_table() -> HashMap<&'static str, Box<dyn Command>> {
let mut m: HashMap<&'static str, Box<dyn Command>> = HashMap::new();
let mut m: HashMap<&'static str, Box<dyn Command>> = HashMap::default();
for c in core_user::commands()
.into_iter()
.chain(core_channel::commands())

View file

@ -4,7 +4,7 @@
//! no manual free, no dangling data.
use std::any::{Any, TypeId};
use std::collections::HashMap;
use crate::map::HashMap;
#[derive(Default)]
pub struct Extensible {

View file

@ -2,7 +2,7 @@
//! and turns a stream of [`Event`]s into IRC. Runs on one thread, so no state is
//! ever locked.
use std::collections::HashMap;
use crate::map::HashMap;
use std::net::{SocketAddr, TcpStream};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender};

View file

@ -22,6 +22,7 @@ pub mod extensible;
pub mod http;
pub mod ircd;
pub mod link;
pub mod map;
pub mod message;
pub mod mode;
pub mod module;

View file

@ -18,7 +18,7 @@
use std::net::{SocketAddr, TcpStream};
use std::collections::HashSet;
use crate::map::HashSet;
use crate::channels::{glob_match, Ban, ChanModes, Channel, Member, Topic};
use crate::message::Message;
@ -1549,7 +1549,7 @@ impl Server {
/// The distinct links a channel's remote members sit behind (minus `except`).
fn channel_link_targets(&self, key: &str, except: Option<Uid>) -> Vec<Uid> {
let mut set: HashSet<Uid> = HashSet::new();
let mut set: HashSet<Uid> = HashSet::default();
if let Some(ch) = self.channels.get(key) {
for uuid in ch.rmembers.keys() {
if let Some(ru) = self.remote_users.get(uuid) {
@ -1739,7 +1739,7 @@ impl Server {
Some(ru) => ru.prefix(),
None => return,
};
let mut notify: HashSet<Uid> = HashSet::new();
let mut notify: HashSet<Uid> = HashSet::default();
let chans: Vec<String> = self
.channels
.iter()
@ -2689,7 +2689,7 @@ mod tests {
use crate::config::Config;
use crate::extensible::Extensible;
use crate::users::{Caps, UserFlags};
use std::collections::HashSet;
use crate::map::HashSet;
use std::sync::atomic::AtomicU64;
use std::sync::{mpsc, Arc};
@ -2737,7 +2737,7 @@ mod tests {
cap_302: false,
caps: Caps::default(),
sasl_mech: None,
channels: HashSet::new(),
channels: HashSet::default(),
watch: Vec::new(),
monitor: Vec::new(),
silence: Vec::new(),
@ -2774,7 +2774,7 @@ mod tests {
use crate::config::Config;
use crate::extensible::Extensible;
use crate::users::{Caps, UserFlags};
use std::collections::HashSet;
use crate::map::HashSet;
use std::sync::atomic::AtomicU64;
use std::sync::{mpsc, Arc};
@ -2824,7 +2824,7 @@ mod tests {
cap_302: false,
caps,
sasl_mech: None,
channels: HashSet::new(),
channels: HashSet::default(),
watch: Vec::new(),
monitor: Vec::new(),
silence: Vec::new(),

View file

@ -3,6 +3,11 @@
//! feed it connections.
#![forbid(unsafe_code)]
// mimalloc as the global allocator: an IRC core allocates a short-lived String per
// message per recipient (tags + body), so allocator throughput is on the hot path.
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::net::TcpListener;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;

16
src/map.rs Normal file
View file

@ -0,0 +1,16 @@
//! Project-wide `HashMap` / `HashSet` backed by [aHash] instead of the standard
//! library's SipHash.
//!
//! The router touches maps on every single message — `users` by uid, `nick_index`
//! and `remote_nick` by nick, `channels` by name, per-channel `members`. SipHash is
//! deliberately slow (it trades speed for DoS resistance); aHash keeps the
//! DoS resistance (its state is seeded from process-random data, so an attacker
//! can't predict bucket placement to force collisions) while hashing these short
//! keys 2-3x faster. Same std `HashMap<K, V, S>` underneath — every method, the
//! `entry` API and `Index` all behave exactly as before; only the hasher changes,
//! so construction moves from `::new()` (SipHash-only) to `::default()`.
//!
//! [aHash]: https://docs.rs/ahash
pub type HashMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;
pub type HashSet<T> = std::collections::HashSet<T, ahash::RandomState>;

View file

@ -19,7 +19,7 @@
//! acctregister_ratecount = 3 max REGISTER attempts per IP …
//! acctregister_ratetime = 3600 … per this many seconds
use std::collections::HashMap;
use crate::map::HashMap;
use crate::command::{CmdResult, Command};
use crate::http::{json_str, urlencode};

View file

@ -6,7 +6,7 @@
//! sender is on (>1). Off unless `blockamsg = yes`. Per-user last-message state
//! lives in `Server.ext`.
use std::collections::HashMap;
use crate::map::HashMap;
use crate::module::{ModResult, Module};
use crate::server::{now, Server};

View file

@ -5,7 +5,8 @@
//! records into it via [`record`], and the CHATHISTORY and REDACT commands
//! read/edit it here.
use std::collections::{HashMap, VecDeque};
use crate::map::HashMap;
use std::collections::VecDeque;
use crate::channels::RANK_HALFOP;
use crate::command::{CmdResult, Command};

View file

@ -6,7 +6,7 @@
//! Off unless `cloudflare_challenge = yes` with `cloudflare_secret` +
//! `cloudflare_url` set. The passed-verification set lives in `Server.ext`.
use std::collections::HashSet;
use crate::map::HashSet;
use crate::command::{CmdResult, Command};
use crate::http::json_str;

View file

@ -8,7 +8,7 @@
//! z-lines match by glob, not CIDR, so the banned range is emitted as a wildcard
//! mask (`1.2.3.*` for an IPv4 /24, the exact IP for a /32).
use std::collections::HashMap;
use crate::map::HashMap;
use std::net::IpAddr;
use crate::module::Module;

View file

@ -2,7 +2,7 @@
//! `connflood = <max> <secs>`. Per-IP recent-connect times live in `Server.ext`,
//! pruned on the tick.
use std::collections::HashMap;
use crate::map::HashMap;
use std::net::IpAddr;
use crate::module::Module;

View file

@ -4,7 +4,7 @@
//! sharing that identity. The store lives in `Server.ext`, cleaned up by the
//! on_user_quit hook.
use std::collections::HashMap;
use crate::map::HashMap;
use crate::command::{CmdResult, Command};
use crate::module::Module;

View file

@ -2,7 +2,7 @@
//! and channels, op-gated, with change notices in a `metadata` batch. The store
//! lives in `Server.ext`, cleaned up by the on_user_quit hook.
use std::collections::HashMap;
use crate::map::HashMap;
use crate::channels::RANK_HALFOP;
use crate::command::{CmdResult, Command};

View file

@ -5,7 +5,7 @@
//! messages. Limits: multiline_maxbytes / multiline_maxlines. In-flight batches live
//! in `Server.ext`, cleaned up by the on_user_quit hook.
use std::collections::HashMap;
use crate::map::HashMap;
use crate::command::{CmdResult, Command};
use crate::coremods::core_message::deliver;

View file

@ -13,7 +13,7 @@
//! `recaptcha_secret` and `recaptcha_url` are set. All config-driven; the only
//! state (who has passed) lives in `Server.ext`.
use std::collections::HashSet;
use crate::map::HashSet;
use crate::command::{CmdResult, Command};
use crate::http::json_str;

View file

@ -4,7 +4,7 @@
//! `reputationexpire` rules and persist to disk. Exposes the `y:` score extban, WHOIS
//! visibility, and the `REPUTATION` oper command. Config-driven (see `[reputation_*]`).
use std::collections::HashMap;
use crate::map::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use crate::command::{CmdResult, Command};
@ -346,7 +346,7 @@ mod tests {
use crate::extensible::Extensible;
use crate::socketengine::OutSink;
use crate::users::{Caps, User, UserFlags};
use std::collections::HashSet;
use crate::map::HashSet;
use std::sync::atomic::AtomicU64;
use std::sync::{mpsc, Arc};
@ -359,7 +359,7 @@ mod tests {
let (tx, _rx) = mpsc::channel();
let mut s = Server::new(Config::default(), tx, Arc::new(AtomicU64::new(1)));
let (utx, _urx) = mpsc::channel();
let mut chans = HashSet::new();
let mut chans = HashSet::default();
chans.insert("#echoircd".to_string());
s.users.insert(
1,

View file

@ -9,7 +9,7 @@
//! IP. Runs off the core thread (never blocks the daemon), bounded in time (the UDP
//! read timeout) and in concurrency (`try_acquire`).
use std::collections::HashMap;
use crate::map::HashMap;
use std::net::{IpAddr, Ipv4Addr, ToSocketAddrs, UdpSocket};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
@ -79,7 +79,7 @@ pub fn release() {
/// and that host resolves back to `ip`. Cached by IP so reconnects and clients
/// behind the same NAT resolve instantly.
pub fn reverse_confirmed(ip: IpAddr, timeout: Duration) -> Option<String> {
let cache = RDNS_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let cache = RDNS_CACHE.get_or_init(|| Mutex::new(HashMap::default()));
if let Ok(g) = cache.lock() {
if let Some((val, exp)) = g.get(&ip) {
if Instant::now() < *exp {
@ -208,7 +208,7 @@ fn ptr_lookup(ns: &str, qname: &str, timeout: Duration) -> Option<String> {
/// `<reversed-ip>.<zone>` name and calls this to test a listing. Cached by qname
/// so repeat DNSBL checks for the same IP+zone don't re-hit the network.
pub fn a_lookup(qname: &str, timeout: Duration) -> Option<Ipv4Addr> {
let cache = A_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let cache = A_CACHE.get_or_init(|| Mutex::new(HashMap::default()));
if let Ok(g) = cache.lock() {
if let Some((val, exp)) = g.get(qname) {
if Instant::now() < *exp {

View file

@ -5,7 +5,8 @@
//! single core thread ever holds a `Server`.
use std::cell::RefCell;
use std::collections::{HashMap, HashSet, VecDeque};
use crate::map::{HashMap, HashSet};
use std::collections::VecDeque;
use std::net::{SocketAddr, TcpStream};
use std::sync::atomic::AtomicU64;
use std::sync::mpsc::Sender;
@ -182,9 +183,9 @@ impl Server {
network: cfg.network,
created: now(),
motd: cfg.motd,
users: HashMap::new(),
nick_index: HashMap::new(),
channels: HashMap::new(),
users: HashMap::default(),
nick_index: HashMap::default(),
channels: HashMap::default(),
events: VecDeque::new(),
opers: cfg.opers,
cloak_key: cfg.cloak_key,
@ -192,13 +193,13 @@ impl Server {
sid: cfg.sid,
server_desc: cfg.serverdesc,
link_blocks: cfg.links,
links: HashMap::new(),
servers: HashMap::new(),
links: HashMap::default(),
servers: HashMap::default(),
uuid_counter: 0,
msgid_counter: 0,
uuid_local: HashMap::new(),
remote_users: HashMap::new(),
remote_nick: HashMap::new(),
uuid_local: HashMap::default(),
remote_users: HashMap::default(),
remote_nick: HashMap::default(),
whowas: VecDeque::new(),
conf_path: cfg.conf_path,
xlines: Vec::new(),
@ -362,7 +363,7 @@ impl Server {
cap_302: false,
caps: Caps::default(),
sasl_mech: None,
channels: HashSet::new(),
channels: HashSet::default(),
watch: Vec::new(),
monitor: Vec::new(),
silence: Vec::new(),
@ -531,12 +532,12 @@ impl Server {
/// content wins), so a backed-up writer stays bounded at one pending snapshot per
/// file — safe precisely because each write is the complete current state.
pub fn disk_write(&self, path: String, contents: String) {
use std::collections::HashMap;
use crate::map::HashMap;
use std::sync::{Condvar, Mutex, OnceLock};
type Pending = std::sync::Arc<(Mutex<HashMap<String, String>>, Condvar)>;
static WRITER: OnceLock<Pending> = OnceLock::new();
let pending = WRITER.get_or_init(|| {
let p: Pending = std::sync::Arc::new((Mutex::new(HashMap::new()), Condvar::new()));
let p: Pending = std::sync::Arc::new((Mutex::new(HashMap::default()), Condvar::new()));
let worker = p.clone();
std::thread::spawn(move || {
let (lock, cv) = &*worker;
@ -674,7 +675,7 @@ impl Server {
}
if user.registered {
let line = format!(":{} QUIT :{reason}", user.prefix());
let mut seen: HashSet<Uid> = HashSet::new();
let mut seen: HashSet<Uid> = HashSet::default();
for key in &user.channels {
if let Some(ch) = self.channels.get_mut(key) {
// +D delayjoin: if their JOIN here was never announced, no QUIT either
@ -1024,7 +1025,7 @@ impl Server {
.get(&uid)
.map(|u| u.channels.iter().cloned().collect())
.unwrap_or_default();
let mut seen: HashSet<Uid> = HashSet::new();
let mut seen: HashSet<Uid> = HashSet::default();
for k in &chans {
if let Some(ch) = self.channels.get(k) {
for &m in ch.members.keys() {
@ -1261,7 +1262,7 @@ mod tests {
cap_302: false,
caps: Caps::default(),
sasl_mech: None,
channels: HashSet::new(),
channels: HashSet::default(),
watch: Vec::new(),
monitor: Vec::new(),
silence: Vec::new(),

View file

@ -15,7 +15,7 @@
//! Both hand the core the same [`OutSink`] output handle, so the core never
//! knows or cares which model a connection uses.
use std::collections::{HashMap, HashSet};
use crate::map::{HashMap, HashSet};
use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::{IpAddr, Shutdown, SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicU64, Ordering};
@ -324,7 +324,7 @@ impl AcceptLimiter {
rate: rate as f64,
burst: burst.max(1) as f64,
inner: Mutex::new(LimiterState {
buckets: HashMap::new(),
buckets: HashMap::default(),
last_prune: Instant::now(),
}),
}))
@ -484,7 +484,7 @@ fn reactor_loop(
max_sendq: usize,
handshake_timeout: Option<Duration>,
) {
let mut conns: HashMap<usize, Conn> = HashMap::new();
let mut conns: HashMap<usize, Conn> = HashMap::default();
let mut next_token = FIRST_CONN;
let mut events = Events::with_capacity(1024);
// TLS conns still negotiating, with the deadline by which they must finish; a
@ -606,7 +606,7 @@ fn reactor_loop(
}
}
// drain everything the core queued, then flush the touched conns
let mut touched: HashSet<usize> = HashSet::new();
let mut touched: HashSet<usize> = HashSet::default();
while let Ok(msg) = out_rx.try_recv() {
match msg {
Out::Line(t, line) => {
@ -810,7 +810,7 @@ fn read_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
}
}
if !c.proxy_pending {
while let Some(pos) = c.rbuf.iter().position(|&b| b == b'\n') {
while let Some(pos) = memchr::memchr(b'\n', &c.rbuf) {
let raw: Vec<u8> = c.rbuf.drain(..=pos).collect();
let text = String::from_utf8_lossy(&raw);
let l = text.trim_end_matches(['\r', '\n']);
@ -1201,7 +1201,7 @@ fn tls_conn(
Ok(0) => break, // EOF
Ok(n) => {
acc.extend_from_slice(&chunk[..n]);
while let Some(pos) = acc.iter().position(|&b| b == b'\n') {
while let Some(pos) = memchr::memchr(b'\n', &acc) {
let raw: Vec<u8> = acc.drain(..=pos).collect();
let text = String::from_utf8_lossy(&raw);
let l = text.trim_end_matches(['\r', '\n']);

View file

@ -5,7 +5,7 @@
//! This backend is openssl. An alternative backend (e.g. rustls) only has to
//! implement these same two traits and it slots straight in.
use std::collections::HashMap;
use crate::map::HashMap;
use std::io::{self, Read, Write};
use std::net::{Shutdown, TcpStream};
use std::sync::{Arc, OnceLock, RwLock};
@ -112,7 +112,7 @@ fn build_ctx(cert: &str, key: &str) -> io::Result<SslContext> {
/// Build the acceptor for the primary cert, with a servername callback that
/// switches to a per-hostname context when the client's SNI matches an `sni` entry.
fn build_acceptor(primary: &CertPaths, sni: &[(String, CertPaths)]) -> io::Result<SslAcceptor> {
let mut map: HashMap<String, SslContext> = HashMap::new();
let mut map: HashMap<String, SslContext> = HashMap::default();
for (host, cp) in sni {
map.insert(host.to_ascii_lowercase(), build_ctx(&cp.cert, &cp.key)?);
}

View file

@ -1,7 +1,7 @@
//! Users: the `User` record plus nick handling, user modes, oper status and the
//! registration/welcome burst.
use std::collections::HashSet;
use crate::map::HashSet;
use std::net::{SocketAddr, TcpStream};
use crate::extensible::Extensible;
@ -486,7 +486,7 @@ impl Server {
}
if registered {
let line = format!(":{prefix} NICK :{newnick}");
let mut targets: HashSet<Uid> = HashSet::new();
let mut targets: HashSet<Uid> = HashSet::default();
targets.insert(uid);
let chans: Vec<String> = self.users[&uid].channels.iter().cloned().collect();
for key in &chans {