Skip to content

Instantly share code, notes, and snippets.

@bassmanitram
Last active December 8, 2023 15:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bassmanitram/d21157c12d109b73fa23cb086b9b3e03 to your computer and use it in GitHub Desktop.
Save bassmanitram/d21157c12d109b73fa23cb086b9b3e03 to your computer and use it in GitHub Desktop.
Infantile comparison between http::HeaderMap and stde::collections::HashMap
[package]
name = "test-map"
version = "0.1.0"
edition = "2021"
[features]
headermap = []
[dependencies]
ahash = "0.8.6"
http = "1.0.0"
//
// A fairly brute-force way to compare http::HeaderMap<HeaderValue> with
// HashMap<HeaderName,Vec<HeaderValue>>, although I am iterating on it to make
// it more sophisticated as time goes by.
//
// It's not a subtle benchmark - just suck in a bunch of headers from a file
// then run operations a stupid number of times. If you build the feature "headermap"
// you get the http::HeaderMap implementation. Otherwise you get the HashMap implementation.
//
// The HashMap implementation uses the ahash crate to improve on hashing.
//
// Where we expect return values I try to use them in the minimal amount of busy work
// I can think of to avoid them being optimized out.
//
// Run:
//
// cargo run --release 2>/dev/null
// cargo run --release --feature headermap 2>/dev/null
//
// HashMap (with ahash) significantly outguns HeaderMap on every test.
//
use std::{fs::File, io::{BufRead, BufReader}, time::Instant};
use http::{HeaderName, HeaderValue};
#[cfg(not(feature = "headermap"))]
#[cfg(feature = "headermap")]
use crate::header_map::HeaderMap;
#[cfg(not(feature = "headermap"))]
pub mod header_map {
use std::collections::HashMap;
use ahash::RandomState;
use http::{HeaderValue, HeaderName};
pub struct Drain<'a> {
m_drain: std::collections::hash_map::Drain<'a, HeaderName, Vec<HeaderValue>>,
v_drain: Option<ValueDrain<>>,
}
pub struct ValueDrain {
pub hn: Option<HeaderName>,
pub drain: Option<std::vec::IntoIter<HeaderValue>>,
}
impl ValueDrain {
fn new(hn: HeaderName, v: Vec<HeaderValue>) -> Self {
Self {
hn: Some(hn),
drain: Some(v.into_iter())
}
}
}
impl<'a,'b> Iterator for Drain<'a> {
type Item = (Option<HeaderName>, HeaderValue);
fn next(&mut self) -> Option<Self::Item> {
if self.v_drain.is_none() {
let Some((hn, v)) = self.m_drain.next() else { return None };
self.v_drain = Some(ValueDrain::new(hn, v));
};
loop {
let v_drain: &mut ValueDrain = self.v_drain.as_mut().unwrap();
let Some(value) = v_drain.drain.as_mut().unwrap().next() else {
let Some((hn, v)) = self.m_drain.next() else { return None };
self.v_drain = Some(ValueDrain::new(hn, v));
continue;
};
return Some(( v_drain.hn.take(), value ));
}
}
}
pub struct Values<'a> {
m_iter: std::collections::hash_map::Values<'a, HeaderName, Vec<HeaderValue>>,
v_iter: Option<std::slice::Iter<'a,HeaderValue>>,
}
impl<'a> Iterator for Values<'a> {
type Item = &'a HeaderValue;
fn next(&mut self) -> Option<Self::Item> {
if self.v_iter.is_none() {
let Some(v) = self.m_iter.next() else { return None };
self.v_iter = Some(v.iter());
};
loop {
let v_iter = self.v_iter.as_mut().unwrap();
let Some(value) = v_iter.next() else {
let Some(v) = self.m_iter.next() else { return None };
self.v_iter = Some(v.iter());
continue;
};
return Some(value);
}
}
}
pub struct ValuesMut<'a> {
m_iter: std::collections::hash_map::ValuesMut<'a, HeaderName, Vec<HeaderValue>>,
v_iter: Option<std::slice::IterMut<'a,HeaderValue>>,
}
impl<'a> Iterator for ValuesMut<'a> {
type Item = &'a mut HeaderValue;
fn next(&mut self) -> Option<Self::Item> {
if self.v_iter.is_none() {
let Some(v) = self.m_iter.next() else { return None };
self.v_iter = Some(v.iter_mut());
};
loop {
let v_iter = self.v_iter.as_mut().unwrap();
let Some(value) = v_iter.next() else {
let Some(v) = self.m_iter.next() else { return None };
self.v_iter = Some(v.iter_mut());
continue;
};
return Some(value);
}
}
}
pub struct HeaderMap {
m: HashMap::<HeaderName, Vec<HeaderValue>, RandomState>,
nv: usize,
}
impl HeaderMap {
pub fn new() -> Self {
HeaderMap{m: HashMap::default(), nv: 0}
}
pub fn with_capacity(capacity: usize) -> Self {
HeaderMap{m: HashMap::with_capacity_and_hasher(capacity, RandomState::new()), nv: 0}
}
pub fn append(&mut self, key: HeaderName, value: HeaderValue) -> bool {
self.nv += 1;
match self.m.get_mut(&key) {
Some(v) => {v.push(value); true},
None => { self.m.insert(key, vec!(value)); false}
}
}
pub fn capacity(&self) -> usize {
self.m.capacity()
}
pub fn clear(&mut self) {
self.m.clear();
self.nv = 0;
}
pub fn contains_key(&mut self, key: &HeaderName) -> bool {
self.m.contains_key(key)
}
pub fn drain<'a,'b>(&'a mut self) -> Drain<'a>
where 'b: 'a {
self.nv = 0;
Drain { m_drain: self.m.drain(), v_drain: None }
}
pub fn entry<K>(&mut self, _key: &HeaderName) {// -> Entry<'_, T>
todo!()
}
pub fn get(&self, key: &HeaderName) -> Option<&HeaderValue> {
self.m.get(key).and_then(|v| if v.is_empty() {None} else {Some(&v[0])})
}
pub fn get_all(&self, key: &HeaderName) -> Option<&Vec<HeaderValue>> {
self.m.get(key)
}
pub fn get_mut(&mut self, key: &HeaderName) -> Option<&mut Vec<HeaderValue>> {
self.m.get_mut(key)
}
pub fn insert<K>(&mut self, key: HeaderName, val: HeaderValue) -> Option<HeaderValue> {
self.nv += 1;
let Some(old_val) = self.m.insert(key, vec!(val)) else { return None; };
self.nv -= old_val.len();
old_val.into_iter().next()
}
pub fn is_empty(&self) -> bool {
self.m.is_empty()
}
pub fn keys(&self) -> std::collections::hash_map::Keys<'_, HeaderName, Vec<HeaderValue>> {
self.m.keys()
}
pub fn keys_len(&self) -> usize {
self.m.len()
}
pub fn len(&self) -> usize {
self.nv
}
pub fn remove(&mut self, key: &HeaderName) -> Option<HeaderValue> {
let Some(v) = self.m.remove(key) else { return None; };
self.nv -= v.len();
v.into_iter().next()
}
pub fn values(&self) -> Values {
Values { m_iter: self.m.values(), v_iter: None }
}
pub fn values_mut(&mut self) -> ValuesMut {
ValuesMut { m_iter: self.m.values_mut(), v_iter: None }
}
}
}
#[cfg(feature = "headermap")]
mod header_map {
use http::{HeaderValue, HeaderName, HeaderMap as HMap};
pub type HeaderMap = HMap::<HeaderValue>;
}
fn map_it(map: &mut header_map::HeaderMap, ents: &Vec::<(HeaderName, HeaderValue)>) {
for (hn, hv) in ents {
map.append(hn.clone(), hv.clone());
}
}
fn find_it(map: &mut header_map::HeaderMap, ents: &Vec::<(HeaderName, HeaderValue)>) -> usize {
let mut count: usize = 0;
for (hn, _hv) in ents {
if let Some(val) = map.get(hn) {
//
// Use the value to avoid it being optimized out
//
count += val.len();
}
}
count
}
fn contains_key(map: &mut header_map::HeaderMap, ents: &Vec::<(HeaderName, HeaderValue)>) -> usize {
let mut count: usize = 0;
for (hn, _hv) in ents {
//
// Use the value to avoid it being optimized out
//
count += usize::from(map.contains_key(hn));
}
count
}
fn values(map: &mut header_map::HeaderMap, ents: &Vec::<(HeaderName, HeaderValue)>) -> usize {
let mut count: usize = 0;
for (hn, _hv) in ents {
//
// Use the value to avoid it being optimized out
//
count += usize::from(map.values().count());
}
count
}
#[cfg(not(feature = "headermap"))]
fn get_all(map: &mut header_map::HeaderMap, ents: &Vec::<(HeaderName, HeaderValue)>) -> usize {
let mut count: usize = 0;
for (hn, _hv) in ents {
if let Some(vals) = map.get_all(hn) {
for val in vals.iter() {
//
// Use each value to avoid it being optimized out
//
count += val.to_str().unwrap().len();
}
}
}
count
}
#[cfg(feature = "headermap")]
fn get_all(map: &mut header_map::HeaderMap, ents: &Vec::<(HeaderName, HeaderValue)>) -> usize {
let mut count: usize = 0;
for (hn, _hv) in ents {
for val in map.get_all(hn).iter() {
//
// Use each value to avoid it being optimized out
//
count += val.to_str().unwrap().len();
}
}
count
}
fn lose_it(map: &mut header_map::HeaderMap, ents: &Vec::<(HeaderName, HeaderValue)>) {
for (hn, _hv) in ents {
map.remove(hn);
}
}
fn main() {
let file = File::open("test-data.txt").expect("Could not open file");
let mut ents = Vec::<(HeaderName, HeaderValue)>::new();
for line in BufReader::new(file).lines().map_while(Result::ok) {
let Some((key, value)) = line.split_once(':') else { continue };
let hn: HeaderName = HeaderName::from_bytes(key.as_bytes()).expect("Bad header name");
let hv: HeaderValue = HeaderValue::from_str(value).expect("Bad header value");
ents.push((hn,hv));
}
let n = 10000;
let mut map = header_map::HeaderMap::with_capacity(ents.len());
let start = Instant::now();
for _i in 0..n {
map_it(&mut map, &ents)
}
println!("Append: {}", start.elapsed().as_micros());
let start = Instant::now();
let mut i: usize = 0;
for _i in 0..n {
i += find_it(&mut map, &ents)
}
println!("Get: {} ({})", start.elapsed().as_micros(), i);
let start = Instant::now();
let mut i: usize = 0;
for _i in 0..100 {
i += get_all(&mut map, &ents)
}
println!("Get all: {} ({})", start.elapsed().as_micros(), i);
let start = Instant::now();
let mut i: usize = 0;
for _i in 0..n {
i += contains_key(&mut map, &ents)
}
println!("Contains: {} ({})", start.elapsed().as_micros(), i);
let start = Instant::now();
let mut i: usize = 0;
for _i in 0..100 {
i += values(&mut map, &ents)
}
println!("Values: {} ({})", start.elapsed().as_micros(), i);
let start = Instant::now();
for _i in 0..n {
lose_it(&mut map, &ents)
}
println!("Remove: {}", start.elapsed().as_micros());
}
Content-Length: 233
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip, deflate, br
CloudFront-Forwarded-Proto: https
CloudFront-Viewer-Country: ES
Host: api.sandbox-113.hub-52.sandbox.assure.dxc.com
Postman-Token: 282fd703-3c20-46e9-b1d8-721b53501a64
User-Agent: Amazon CloudFront
Via: 1.1 24c7a1aab30c05f7e2022ac9ddfdef7a.cloudfront.net (CloudFront)
X-Amz-Cf-Id: 2ioPhGFV_oUEWxEQo0oAc4ATsa3OUz0A3EbVOOSqF5FlLRkTa7recg==
X-Amzn-Trace-Id: Root=1-620e1ee7-071242243649b6eb4fc4fdad
X-DXC-Behaviour-Prefix: /api
X-DXC-Original-Host: sandbox-113.hub-52.sandbox.assure.dxc.com
X-DXC-Original-URL: https://sandbox-113.hub-52.sandbox.assure.dxc.com/api/assure-access-management/acls/CSAM_DEV
X-Forwarded-For: 213.177.195.117, 64.252.83.195
X-Forwarded-Port: 443
X-Forwarded-Proto: https
my-url-header: https://sandbox-113.hub-52.sandbox.assure.dxc.com/api/assure-access-management/acls/CSAM_DEV/martins/url/header
Content-Length: 233
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip, deflate, br
CloudFront-Forwarded-Proto: https
CloudFront-Viewer-Country: ES
Host: api.sandbox-113.hub-52.sandbox.assure.dxc.com
Postman-Token: 282fd703-3c20-46e9-b1d8-721b53501a64
User-Agent: Amazon CloudFront
Via: 1.1 24c7a1aab30c05f7e2022ac9ddfdef7a.cloudfront.net (CloudFront)
X-Amz-Cf-Id: 2ioPhGFV_oUEWxEQo0oAc4ATsa3OUz0A3EbVOOSqF5FlLRkTa7recg==
X-Amzn-Trace-Id: Root=1-620e1ee7-071242243649b6eb4fc4fdad
X-DXC-Behaviour-Prefix: /api
X-DXC-Original-Host: sandbox-113.hub-52.sandbox.assure.dxc.com
X-DXC-Original-URL: https://sandbox-113.hub-52.sandbox.assure.dxc.com/api/assure-access-management/acls/CSAM_DEV
X-Forwarded-For: 213.177.195.117, 64.252.83.195
X-Forwarded-Port: 443
X-Forwarded-Proto: https
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment