# [help]Improving the performance of this code

I have written a code using genetic algorithm in OCaml and Rust, the performance measuring results show that the code in OCaml is 2.5 times slower than Rust, could someone give me some tips for optimizing my OCaml code?
Here is the code:
OCaml version 4.14.0
build command: ocamlopt -o gen.exe -O3 -unsafe gen.ml
execute command: ./gen.exe

let _MIN_X = 0.
let _MAX_X = 10.

(* [min, max] *)
let randInt ~min ~max =
let ratio = Random.float 1. in
let value =
(((max |> float_of_int) -. (min |> float_of_int)) *. ratio)
+. (min |> float_of_int)
in
value |> floor |> int_of_float

module Chrome = struct
type t = { mutable x : float; mutable fitness : float }

let init () = { x = Random.float (_MAX_X -. _MIN_X) +. _MIN_X; fitness = 0. }
let calculate c = c.fitness <- c.x *. c.x *. c.x

let mutate () =
let c = init () in
let () = c |> calculate in
c

let cross c1 c2 =
let median = (c1.x +. c2.x) /. 2. in
let newC1 = { x = 0.; fitness = 0. } in
let newC2 = { x = 0.; fitness = 0. } in
let () = newC1.x <- Random.float (median -. _MIN_X) +. _MIN_X in
let () = newC1 |> calculate in
let () = newC2.x <- Random.float (_MAX_X -. median) +. median in
let () = newC2 |> calculate in
(newC1, newC2)
end

module Gen = struct
type t = {
mutable chromes : Chrome.t array;
genSize : int;
genCount : int;
mutable bestChrome : Chrome.t option;
mutRatio : float;
crossRatio : float;
}

let init g =
g.chromes <-
Array.init g.genSize (fun _i ->
let c = Chrome.init () in
let () = c |> Chrome.calculate in
c)

let mutate g =
for i = 0 to g.genSize - 1 do
let ratio = Random.float 1. in
if ratio <= g.mutRatio then
let c = Chrome.mutate () in
(g.chromes |> Array.set) i c
done

let cross g =
for _i = 0 to g.genSize - 1 do
let ratio = Random.float 1. in
if ratio <= g.crossRatio then
let i1, i2 =
( randInt ~min:0 ~max:(g.genSize - 1),
randInt ~min:0 ~max:(g.genSize - 1) )
in
let c1, c2 = Chrome.cross g.chromes.(i1) g.chromes.(i2) in
let () = g.chromes.(i1) <- c1 in
g.chromes.(i2) <- c2
done

let updateGen g =
let () =
Array.sort
(fun (c1 : Chrome.t) (c2 : Chrome.t) ->
if c1.fitness < c2.fitness then -1
else if c1.fitness == c2.fitness then 0
else 1)
g.chromes
in
g.bestChrome <- Some g.chromes.(0)

let start g =
for _i = 0 to g.genCount - 1 do
let () = g |> mutate in
let () = g |> cross in
g |> updateGen
done
end

let start () =
let gen : Gen.t =
{
chromes = ([||] : Chrome.t array);
genSize = 10000;
genCount = 1000;
bestChrome = None;
mutRatio = 0.15;
crossRatio = 0.65;
}
in
let () = gen |> Gen.init in
let () = gen |> Gen.start in
match gen.bestChrome with
| None -> ()
| Some c ->
print_endline
("best value is "
^ (c.fitness |> string_of_float)
^ " and best x is " ^ (c.x |> string_of_float))

let main () =
let testCount = 10 in
let timeStart = Sys.time () in
let () =
for _i = 0 to testCount - 1 do
start ()
done
in
let timeEnd = Sys.time () in
print_endline
("mean time is "
^ (let timeUse =
(timeEnd -. timeStart) *. 1000. /. (testCount |> float_of_int)
in
timeUse |> Float.round |> int_of_float |> string_of_int)
^ "ms")

let () = main ()

running result

best value is 3.96616750408e-10 and best x is 0.00073472308226
best value is 3.2272451753e-11 and best x is 0.000318378686536
best value is 1.23686587687e-12 and best x is 0.000107343117167
best value is 3.68554344258e-10 and best x is 0.000716969220129
best value is 7.59396016819e-10 and best x is 0.000912338715952
best value is 1.88381205775e-10 and best x is 0.000573252368078
best value is 4.40679331038e-11 and best x is 0.000353216427623
best value is 3.53500191601e-09 and best x is 0.00152333896594
best value is 3.03752916138e-13 and best x is 6.72212863153e-05
best value is 1.52011916399e-10 and best x is 0.000533694275756
mean time is 2926ms

rust version v1.67.0
execute command: cargo run --release
dependencies: rand = â0.8.5â

use std::time;

use rand;

trait Fitness {
fn calculate(&mut self);
}

trait Chrome {
fn init() -> Box<Self>;
fn mutate() -> Box<Self>;
fn cross(c1: &Self, c2: &Self) -> (Box<Self>, Box<Self>);
}

trait Gen {
fn init(&mut self);
fn start(&mut self);
fn mutate(&mut self);
fn cross(&mut self);
fn update_gen(&mut self);
}

struct XChrome {
pub x: f32,
pub fitness: f32,
}

const MIN_X: f32 = 0.;
const MAX_X: f32 = 10.;

impl Chrome for XChrome {
fn init() -> Box<Self> {
Box::new(XChrome {
x: rand::random::<f32>() * (MAX_X - MIN_X),
fitness: 0.,
})
}

fn mutate() -> Box<Self> {
let mut c = XChrome::init();
c.calculate();
c
}

fn cross(c1: &Self, c2: &Self) -> (Box<Self>, Box<Self>) {
let median = (c1.x + c2.x) / 2.;
let mut new_c1 = XChrome {
x: MIN_X,
fitness: 0.,
};
let mut new_c2 = XChrome {
x: MIN_X,
fitness: 0.,
};
new_c1.x = rand::random::<f32>() * (median - MIN_X) + MIN_X;
new_c1.calculate();
new_c2.x = rand::random::<f32>() * (MAX_X - median) + median;
new_c2.calculate();
(Box::new(new_c1), Box::new(new_c2))
}
}

impl Fitness for XChrome {
fn calculate(&mut self) {
self.fitness = self.x * self.x * self.x;
}
}

struct XGen {
pub chromes: Box<Vec<Box<XChrome>>>,
pub gen_size: usize,
pub gen_count: usize,
pub best_chrome: Option<Box<XChrome>>,
pub mut_ratio: f32,
pub cross_ratio: f32,
}

fn rand_int(min: usize, max: usize) -> usize {
let ratio = rand::random::<f32>();
let value = (max as f32 - min as f32) * ratio;
value.floor() as usize
}

impl Gen for XGen {
fn init(&mut self) {
self.chromes = Box::new(Vec::with_capacity(self.gen_size));
for _i in 0..self.gen_size {
let mut chrome = XChrome::init();
chrome.calculate();
self.chromes.push(chrome);
}
}

fn start(&mut self) {
for _i in 0..self.gen_count {
self.mutate();
self.cross();
self.update_gen();
}
}

fn mutate(&mut self) {
for i in 0..self.gen_size {
let ratio = rand::random::<f32>();
if ratio <= self.mut_ratio {
let chrome = XChrome::mutate();
self.chromes[i] = chrome;
}
}
}

fn cross(&mut self) {
for _i in 0..self.gen_size {
let ratio = rand::random::<f32>();
if ratio <= self.cross_ratio {
let (i1, i2) = (rand_int(0, self.gen_size), rand_int(0, self.gen_size));
let (c1, c2) = unsafe {
XChrome::cross(
&self.chromes.get_unchecked(i1),
&self.chromes.get_unchecked(i2),
)
};
// let (c1, c2) = XChrome::cross(&self.chromes[i1], &self.chromes[i2]);
self.chromes[i1] = c1;
self.chromes[i2] = c2;
}
}
}

fn update_gen(&mut self) {
self.chromes
.sort_by(|c1, c2| c1.fitness.partial_cmp(&c2.fitness).unwrap());
self.best_chrome = Some(Box::new(XChrome {
x: self.chromes[0].x,
fitness: self.chromes[0].fitness,
}));
}
}

fn start() {
let mut gen = XGen {
chromes: Box::new(vec![]),
gen_size: 10000,
gen_count: 1000,
best_chrome: None,
mut_ratio: 0.15,
cross_ratio: 0.65,
};
gen.init();
gen.start();
match gen.best_chrome {
None => (),
Some(chrome) => println!(
"best value is {} and best x is {}",
chrome.fitness, chrome.x
),
}
}

fn main() {
let test_count = 10;
let time_start = time::Instant::now();
for _i in 0..test_count {
start()
}
let time_end = time::Instant::now();
let ms = time_end.duration_since(time_start).as_millis();
println!("mean time is {}ms", ms / test_count);
}

running result

best value is 0.00000000000045630557 and best x is 0.00007698721
best value is 0.0000000001450359 and best x is 0.00052540214
best value is 0.000000000076834546 and best x is 0.00042512716
best value is 0.00000000000028120017 and best x is 0.000065514665
best value is 0.0000000028171288 and best x is 0.001412328
best value is 0.000000000003870766 and best x is 0.0001570118
best value is 0.00000000042730455 and best x is 0.0007532038
best value is 0.000000000035633937 and best x is 0.00032906974
best value is 0.000000000028005924 and best x is 0.0003036803
best value is 0.0000000043604453 and best x is 0.0016337174
mean time is 889ms
1 Like

Have you tried using flambda? âThere is no support for a single compiler that can operate in both Flambda and non-Flambda modes.â So you need to pass an option to the opam switch.
https://v2.ocaml.org/manual/flambda.html

Another obvious thing to try is to replace array operations with their unsafe variants Array.unsafe_get, Array.unsafe_set. (You should be able to rebind the operators.)

I Actually have added the unsafe flag into ocamlopt.

-unsafe  Do not compile bounds checking on array and string access
1 Like

A few tips:

• Your randInt function looks way too complicated. If you need a number in the integer interval [x; y), just use x + Random.int (y - x). If you want to also get y from time to time, check for overflow and use y + 1 as the bound.
• You donât seem to need the mutability on the type Chrome.t. Use immutable records, and if youâre using flambda you should notice an improvement.
• In Gen.updateGen, your comparison is just fun c1 c2 -> Float.compare c1.fitness c2.fitness. Youâre introducing unnecessary overhead by spitting the cases manually (plus youâre using physical equality on floats, which is always a bad idea).
• Combining the reverse-application operator (|>) with regular applications creates a risk that the compiler will fail to turn the result into a single direct application. I donât think itâs going to impact performance in your code but I would suggest avoiding that style in general (as in the call to Array.set in Gen.mutate)
1 Like

Is that right? I was under the impression that x |> f is purely syntactic sugar for f x nowadays.

Thanks, i will try it and put the results later!

Itâs not technically syntactic sugar, but with recent compilers it should be equivalent to a regular application. But with multiple arguments you still need to rely on the compiler translating (f x) y into f x y, and the code that does this is a bit tricky, so itâs safer to just use direct applications.

2 Likes

In

let updateGen g =
let () =
Array.sort
(fun (c1 : Chrome.t) (c2 : Chrome.t) ->
if c1.fitness < c2.fitness then -1
else if c1.fitness == c2.fitness then 0
else 1)
g.chromes
in
g.bestChrome <- Some g.chromes.(0)

do you really need to sort g.chromes or do you just need the minimum value?

Not performance related, but if you return g from mutate and cross you could rewrite

let () = g |> mutate in
let () = g |> cross in
g |> updateGen

to

g |> mutate |> cross |> updateGen

which would make the pipes |> a bit more âusualâ. I donât know if that has been your intention of using |>.

Another thing you can do is to replace

let median = (c1.x +. c2.x) /. 2. in

with

let median = (c1.x +. c2.x) *. 0.5 in

But anyway, your biggest issue might be that your OCaml code as a worst cache locality than the Rust one. You can get the data using

1 Like

1. Random.int has a limit value of 230, this is not suitable for my case, cause the int value should varying from 0 to 9999.
2. In the mutate function of module Chrome, it need to create a instance and then caculate the fitness, so i think the fields of Chrome.t should be mutable.
3. I replaced my comparison function with Float.compare
4. Not changed for now.
let _MIN_X = 0.
let _MAX_X = 10.

(* [min, max] *)
let randInt ~min ~max =
let ratio = Random.float 1. in
let value =
(((max |> Float.of_int) -. (min |> Float.of_int)) *. ratio)
+. (min |> Float.of_int)
in
value |> Float.floor |> Int.of_float

module Chrome = struct
type t = { mutable x : float; mutable fitness : float }

let init () = { x = Random.float (_MAX_X -. _MIN_X) +. _MIN_X; fitness = 0. }
let calculate c = c.fitness <- c.x *. c.x *. c.x

let mutate () =
let c = init () in
let () = c |> calculate in
c

let cross c1 c2 =
let median = (c1.x +. c2.x) /. 2. in
let newC1 = { x = 0.; fitness = 0. } in
let newC2 = { x = 0.; fitness = 0. } in
let () = newC1.x <- Random.float (median -. _MIN_X) +. _MIN_X in
let () = newC1 |> calculate in
let () = newC2.x <- Random.float (_MAX_X -. median) +. median in
let () = newC2 |> calculate in
(newC1, newC2)
end

module Gen = struct
type t = {
mutable chromes : Chrome.t array;
genSize : int;
genCount : int;
mutable bestChrome : Chrome.t option;
mutRatio : float;
crossRatio : float;
}

let init g =
g.chromes <-
Array.init g.genSize (fun _i ->
let c = Chrome.init () in
let () = c |> Chrome.calculate in
c)

let mutate g =
for i = 0 to g.genSize - 1 do
let ratio = Random.float 1. in
if Float.compare ratio g.mutRatio != 1 then
let c = Chrome.mutate () in
(g.chromes |> Array.set) i c
done

let cross g =
for _i = 0 to g.genSize - 1 do
let ratio = Random.float 1. in
if Float.compare ratio g.crossRatio != 1 then
let i1, i2 =
( randInt ~min:0 ~max:(g.genSize - 1),
randInt ~min:0 ~max:(g.genSize - 1) )
in
let c1, c2 = Chrome.cross g.chromes.(i1) g.chromes.(i2) in
let () = g.chromes.(i1) <- c1 in
g.chromes.(i2) <- c2
done

let updateGen g =
let () =
Array.sort
(fun (c1 : Chrome.t) (c2 : Chrome.t) ->
Float.compare c1.fitness c2.fitness)
g.chromes
in
g.bestChrome <- Some g.chromes.(0)

let start g =
for _i = 0 to g.genCount - 1 do
let () = g |> mutate in
let () = g |> cross in
g |> updateGen
done
end

let start () =
let gen : Gen.t =
{
chromes = ([||] : Chrome.t array);
genSize = 10000;
genCount = 1000;
bestChrome = None;
mutRatio = 0.15;
crossRatio = 0.65;
}
in
let () = gen |> Gen.init in
let () = gen |> Gen.start in
match gen.bestChrome with
| None -> ()
| Some c ->
Stdlib.prerr_endline
("best value is "
^ (c.fitness |> Float.to_string)
^ " and best x is " ^ (c.x |> Float.to_string))

let main () =
let testCount = 10 in
let timeStart = Sys.time () in
let () =
for _i = 0 to testCount - 1 do
start ()
done
in
let timeEnd = Sys.time () in
Stdlib.print_endline
("mean time is "
^ (let timeUse =
(timeEnd -. timeStart) *. 1000. /. (testCount |> Float.of_int)
in
timeUse |> Float.round |> Int.of_float |> Int.to_string)
^ "ms")

let () = main ()

results

best value is 3.96616750408e-10 and best x is 0.00073472308226
best value is 3.2272451753e-11 and best x is 0.000318378686536
best value is 1.23686587687e-12 and best x is 0.000107343117167
best value is 3.68554344258e-10 and best x is 0.000716969220129
best value is 7.59396016819e-10 and best x is 0.000912338715952
best value is 1.88381205775e-10 and best x is 0.000573252368078
best value is 4.40679331038e-11 and best x is 0.000353216427623
best value is 3.53500191601e-09 and best x is 0.00152333896594
best value is 3.03752916138e-13 and best x is 6.72212863153e-05
best value is 1.52011916399e-10 and best x is 0.000533694275756
mean time is 3377ms

I wonder if you can unbox the floats in Chrome.t so that the chrome array gets flattened.

I need selecting some good items in the group, it is ignored here

The limit is 2^30. Random.int 10000 will work just fine.

Oh, i was misled by the vscode preview of doc. I have replaced my randInt with Random.int

(* [min, max] *)
let randInt ~min ~max = Random.int (max - min) + min

No visible performance changing.

Oooh, boy, an optimisation problem! What fun.

let cross g =
for _i = 0 to g.genSize - 1 do
let ratio = Random.float 1. in
if Float.compare ratio g.crossRatio != 1 then
let i1, i2 =
( randInt ~min:0 ~max:(g.genSize - 1),
randInt ~min:0 ~max:(g.genSize - 1) )
in
let c1, c2 = Chrome.cross g.chromes.(i1) g.chromes.(i2) in
(*[1]*)  let () = g.chromes.(i1) <- c1 in
(*[2]*)  g.chromes.(i2) <- c2
done

The lines [1] and [2] probably will likely trigger the write barrier.

You can avoid it if you rewrite your code so that the cross is done directly by mutating the Chrome.t struct:

let cross_inplace t i1 i2 =
let median = (t.(i1).Chrome.x +. t.(i2).x) *. 0.5 in
t.(i1).x <- Random.float (median -. _MIN_X) +. _MIN_X;
calculate t.(i1);
t.(i1).x <- Random.float (_MAX_X -. median) +. median;
calculate t.(i2)

and update your Gen.cross function as:

let cross g =
for _i = 0 to g.genSize - 1 do
let ratio = Random.float 1. in
if ratio <= g.crossRatio then
let i1, i2 =
( randInt ~min:0 ~max:(g.genSize - 1),
randInt ~min:0 ~max:(g.genSize - 1) )
in
cross_inplace g.chromes i1 i2
done

Also, in:

let updateGen g =
let () =
Array.sort
(fun (c1 : Chrome.t) (c2 : Chrome.t) ->
Float.compare c1.fitness c2.fitness)
g.chromes
in
g.bestChrome <- Some g.chromes.(0)

let start g =
for _i = 0 to g.genCount - 1 do
let () = g |> mutate in
let () = g |> cross in
g |> updateGen
done

you write to g.bestChrome on each call to updateGen in your loop, but as it is overwritten on each loop, this write is just needlessly invoking the write barrier.

let updateGen g =
Array.sort
(fun (c1 : Chrome.t) (c2 : Chrome.t) ->
Float.compare c1.fitness c2.fitness)
g.chromes

let start g =
for _i = 0 to g.genCount - 1 do
let () = g |> mutate in
let () = g |> cross in
g |> updateGen
done;
g.bestChrome <- Some g.chromes.(0)
1 Like

Thanks for the reply, but the instance of Chrome.t shouldât be mutable in chross function, because the function updateGen will copy(not really copy, just repeat using the same object) some items into the group(this step is ignored in my code), and updating bestChrome in every loop is because we want to track the best instance. So, this two places you point out which may invoking the write barrier is necessary, the optimization shouldnât not change the algorithm itself.

Yes, stable_sort is faster than Array.sort with about 700ms.

best value is 5.2671890462e-12 and best x is 0.000173990806823
best value is 2.39369249382e-17 and best x is 2.88196997976e-06
best value is 3.50392541523e-10 and best x is 0.00070499323738
best value is 1.04521736742e-09 and best x is 0.00101485081727
best value is 7.1120663069e-11 and best x is 0.000414316216726
best value is 6.28148092479e-12 and best x is 0.000184510328369
best value is 6.24688116892e-10 and best x is 0.000854845732979
best value is 7.74849079166e-10 and best x is 0.000918485646354
best value is 1.21257054376e-08 and best x is 0.00229739499884
best value is 1.97807675276e-10 and best x is 0.000582658893358
mean time is 2181ms
• Chrome.t doesnât need to be mutable.

mutate () creates a new value from scratch and can be implemented without mutability. The init function is used once and the value it returns is immediately passed to calculate, thatâs the only use of calculate outside of Chrome. Same as for mutate, cross doesnât mutate values passed to it but rather create new values.

module Chrome = struct
type t = { x : float; fitness : float }

let fitness x = x *. x *. x

let mk x = { x; fitness = fitness x }

let init () =
mk (Random.float (_MAX_X -. _MIN_X) +. _MIN_X)

let cross c1 c2 =
let median = (c1.x +. c2.x) /. 2. in
let newC1 = mk (Random.float (median -. _MIN_X) +. _MIN_X) in
let newC2 = mk (Random.float (_MAX_X -. median) +. median) in
(newC1, newC2)
end
• Array.sort is not necessary.

The sorted array is overwritten on the next generation. Only the first field is used (g.bestChrome <- Some g.chromes.(0)). This can be computed linearly with Array.fold.

• Unfortunately, floats are not well optimized in OCaml. Any hope of using fixed point arithmetic ?
1 Like

I rewrite the code form mutable Chrome.t to immutable, no visible performance changed.

let _MIN_X = 0.
let _MAX_X = 10.

(* [min, max] *)
let randInt ~min ~max = Random.int (max - min) + min

module Chrome = struct
type t = { x : float; fitness : float }

let calculate x = x *. x *. x

let init () =
let x = Random.float (_MAX_X -. _MIN_X) +. _MIN_X in
{ x; fitness = calculate x }

let mutate = init

let cross c1 c2 =
let median = (c1.x +. c2.x) /. 2. in
let newC1 =
let x = Random.float (median -. _MIN_X) +. _MIN_X in
{ x; fitness = calculate x }
in
let newC2 =
let x = Random.float (_MAX_X -. median) +. median in
{ x; fitness = calculate x }
in
(newC1, newC2)
end

module Gen = struct
type t = {
mutable chromes : Chrome.t array;
genSize : int;
genCount : int;
mutable bestChrome : Chrome.t option;
mutRatio : float;
crossRatio : float;
}

let init g =
g.chromes <-
Array.init g.genSize (fun _i ->
let c = Chrome.init () in
c)

let mutate g =
for i = 0 to g.genSize - 1 do
let ratio = Random.float 1. in
if Float.compare ratio g.mutRatio != 1 then
let c = Chrome.mutate () in
Array.set g.chromes i c
done

let cross g =
for _i = 0 to g.genSize - 1 do
let ratio = Random.float 1. in
if Float.compare ratio g.crossRatio != 1 then
let i1, i2 =
( randInt ~min:0 ~max:(g.genSize - 1),
randInt ~min:0 ~max:(g.genSize - 1) )
in
let c1, c2 = Chrome.cross g.chromes.(i1) g.chromes.(i2) in
let () = g.chromes.(i1) <- c1 in
g.chromes.(i2) <- c2
done

let updateGen g =
let () =
Array.stable_sort
(fun (c1 : Chrome.t) (c2 : Chrome.t) ->
if c1.fitness <= c2.fitness then -1
else if c1.fitness == c2.fitness then 0
else 1)
g.chromes
in
g.bestChrome <- Some g.chromes.(0)

let start g =
for _i = 0 to g.genCount - 1 do
let () = mutate g in
let () = cross g in
g |> updateGen
done
end

let start () =
let gen : Gen.t =
{
chromes = ([||] : Chrome.t array);
genSize = 10000;
genCount = 1000;
bestChrome = None;
mutRatio = 0.15;
crossRatio = 0.65;
}
in
let () = gen |> Gen.init in
let () = gen |> Gen.start in
match gen.bestChrome with
| None -> ()
| Some c ->
Stdlib.prerr_endline
("best value is "
^ (c.fitness |> Float.to_string)
^ " and best x is " ^ (c.x |> Float.to_string))

let main () =
let testCount = 10 in
let timeStart = Sys.time () in
let () =
for _i = 0 to testCount - 1 do
start ()
done
in
let timeEnd = Sys.time () in
Stdlib.print_endline
("mean time is "
^ (let timeUse =
(timeEnd -. timeStart) *. 1000. /. (testCount |> Float.of_int)
in
timeUse |> Float.round |> Int.of_float |> Int.to_string)
^ "ms")

let () = main ()

results with Array.stable_sort

best value is 5.2671890462e-12 and best x is 0.000173990806823
best value is 2.39369249382e-17 and best x is 2.88196997976e-06
best value is 3.50392541523e-10 and best x is 0.00070499323738
best value is 1.04521736742e-09 and best x is 0.00101485081727
best value is 7.1120663069e-11 and best x is 0.000414316216726
best value is 6.28148092479e-12 and best x is 0.000184510328369
best value is 6.24688116892e-10 and best x is 0.000854845732979
best value is 7.74849079166e-10 and best x is 0.000918485646354
best value is 1.21257054376e-08 and best x is 0.00229739499884
best value is 1.97807675276e-10 and best x is 0.000582658893358
mean time is 2150ms