2
\$\begingroup\$

I'm new to rust and I'm trying to build a card game simulation to find all of the best actions with all possible hands. This requires a lot of simulations and my first 3 million are already taking a huge amount of time. I want to show you 2 variants of my code: first, the one with no threading which takes about 35 minutes to complete, then the one with threading which takes about 50 minutes. I was wondering if there were any obvious places I could optimize my code. For some reason, the compiler didn't want me threading without cloning a lot, which, in addition to the amount of threads is probably the reason it's so slow. So here's the code without threading:

fn test_simulation(cards: &mut Cards, results: &mut Vec<Vec<Results>>, combo_number:u32){
let card = 5;
println!("Combo {}/3072", combo_number+1);
let percentage = 100*combo_number/3071;
println!("{:?}%", percentage);

let vs_card= VS_CARDS[card];
let mut action1_result = [0.0; 3];
let mut action2_result = [0.0; 3];
let mut action3_result = [0.0; 3];
let mut action4_result = [0.0; 3];

cards.vs_card = vs_card;

for i in 0..100{
    

    cards.recut();
 


    if SRR && cards.test_combo.len() == 2{
        let result =  cards.action1();
        action1_result[0] = (result[0]+ action1_result[0]*i as f32)/(i as f32 + 1.0);
        action1_result[1] = (result[1]+ action1_result[1]*i as f32)/(i as f32 + 1.0);
        action1_result[2] = (result[2]+ action1_result[2]*i as f32)/(i as f32 + 1.0);
    }else{
        action1_result[0] = -1.0;
        action1_result[1] = 0.0;
        action1_result[2] = 0.0;
      
    }
    let result = cards.action2();
    action2_result[0] = (result[0]+ action2_result[0]*i as f32)/(i as f32 + 1.0);
    action2_result[1] = (result[1]+ action2_result[1]*i as f32)/(i as f32 + 1.0);
    action2_result[2] = (result[2]+ action2_result[2]*i as f32)/(i as f32 + 1.0);
    
    if D_ALL && cards.test_combo.len() == 2{
        let result = cards.action3();
        action3_result[0] = (result[0]+ action3_result[0]*i as f32)/(i as f32 + 1.0);
        action3_result[1] = (result[1]+ action3_result[1]*i as f32)/(i as f32 + 1.0);
        action3_result[2] = (result[2]+ action3_result[2]*i as f32)/(i as f32 + 1.0);
    }else{
        action3_result[0] = -1.0;
        action3_result[1] = 0.0;
        action3_result[2] = 0.0;
    }

    let result = cards.action4(&mut results[card]);
    action4_result[0] = (result[0]+ action4_result[0]*i as f32)/(i as f32 + 1.0);
    action4_result[1] = (result[1]+ action4_result[1]*i as f32)/(i as f32 + 1.0);
    action4_result[2] = (result[2]+ action4_result[2]*i as f32)/(i as f32 + 1.0);
    
}
let combo = &cards.test_combo;
let mut u8_combo = Card::combo_rank_to_value(combo);
u8_combo.sort();
results[card].push(Results::new(combo.to_vec() ,u8_combo, vs_card, action1_result, action2_result, action3_result,action4_result ));}





fn main() {
    let (mut all_cards, mut all_results) = initialize_all();

 

   // Will test against 12 possible opponent cards, right now using just 1
    for i in 0..all_cards.len(){
        
        test_simulation( &mut all_cards[i], &mut all_results, i as u32);
        
    };

    
    for i in 0..all_results[5].len() {

        if all_results[5][i].u8_combo.len() == 2{
            print!("{:#?} ", all_results[5][i].u8_combo);
            print!("{:#?} ", all_results[5][i].best_action);
            print!("{} ", all_results[5][i].best_action_value);
        }
        
    }
    

}

And here is the slow one with threading and a mess of Arc Mutexes:

    fn test_simulation(cards: &mut Cards, results: &mut Vec<Vec<Results>>, combo_number:u32){
    let mut this_cards = cards.clone();
    let card = 5;
    let current_results = results[5].clone();

    println!("Combo {}/3072", combo_number+1);
    let percentage = 100*combo_number/3071;
    println!("{:?}%", percentage);

    let vs_card= VS_CARDS[card];
    let action1_result = Arc::new(Mutex::new([0.0; 3]));
    let action2_result = Arc::new(Mutex::new([0.0; 3]));
    let action3_result = Arc::new(Mutex::new([0.0; 3]));
    let action4_result = Arc::new(Mutex::new([0.0; 3]));
    let mut handles = vec![];

    this_cards.vs_card = vs_card;

    let  this_cards = Arc::new(Mutex::new(this_cards));
    let  current_results = Arc::new(Mutex::new(current_results));
    for i in 0..100{
        let  this_cards = Arc::clone(&this_cards);
        let  current_results = Arc::clone(&current_results);
        this_cards.lock().unwrap().recut();
        let action1_result = Arc::clone(&action1_result);
        let action2_result = Arc::clone(&action2_result);
        let action3_result = Arc::clone(&action3_result);
        let action4_result = Arc::clone(&action4_result);
        
        let handle = thread::spawn(move || {
        
            let mut action1_result = action1_result.lock().unwrap();
            let mut action2_result = action2_result.lock().unwrap();
            let mut action3_result = action3_result.lock().unwrap();
            let mut action4_result = action4_result.lock().unwrap();
            if SRR && this_cards.lock().unwrap().test_combo_length == 2{
                let act1_res =  this_cards.lock().unwrap().surrender();
                
                action1_result[0] = (act1_res[0]+ action1_result[0]*i as f32)/(i as f32 + 1.0);
                action1_result[1] = (act1_res[1]+ action1_result[1]*i as f32)/(i as f32 + 1.0);
                action1_result[2] = (act1_res[2]+ action1_result[2]*i as f32)/(i as f32 + 1.0);
            }else{
                action1_result[0] = -1.0;
                action1_result[1] = 0.0;
                action1_result[2] = 0.0;
          
                }
            let act2_res = this_shoe.lock().unwrap().stand();
            action2_result[0] = (act2_res[0]+ action2_result[0]*i as f32)/(i as f32 + 1.0);
            action2_result[1] = (act2_res[1]+ action2_result[1]*i as f32)/(i as f32 + 1.0);
            action2_result[2] = (act2_res[2]+ action2_result[2]*i as f32)/(i as f32 + 1.0);
            
            if D_ALL && this_cards.lock().unwrap().test_combo_length == 2{
                let act3_res = this_cards.lock().unwrap().double();
                action3_result[0] = (act3_res[0]+ action3_result[0]*i as f32)/(i as f32 + 1.0);
                action3_result[1] = (act3_res[1]+ action3_result[1]*i as f32)/(i as f32 + 1.0);
                action3_result[2] = (act3_res[2]+ action3_result[2]*i as f32)/(i as f32 + 1.0);
            }else{
                action3_result[0] = -1.0;
                action3_result[1] = 0.0;
                action3_result[2] = 0.0;
            }
    
            let act4_res = this_shoe.lock().unwrap().hit(&mut current_results.lock().unwrap());
            action4_result[0] = (act4_res[0]+ action4_result[0]*i as f32)/(i as f32 + 1.0);
            action4_result[1] = (act4_res[1]+ action4_result[1]*i as f32)/(i as f32 + 1.0);
            action4_result[2] = (act4_res[2]+ action4_result[2]*i as f32)/(i as f32 + 1.0);


            
            
        });
        handles.push(handle);

    }
    for handle in handles {
        handle.join().unwrap();
    }

    let combo = &cards.test_combo;
    let mut u8_combo = Card::combo_rank_to_value(combo);

    u8_combo.sort();
   
    results[card].push(Results::new(combo.to_vec() ,u8_combo, vs_card, *action1_result.lock().unwrap(), *action2_result.lock().unwrap(), *action3_result.lock().unwrap(), *action4_result.lock().unwrap()));
   }

  fn main() {
        let (mut all_cards, mut all_results) = initialize_all();
    
     
    
       // Will test against 12 possible opponent cards, right now using just 1
        for i in 0..all_cards.len(){
            
            test_simulation( &mut all_cards[i], &mut all_results, i as u32);
            
        };
    
        
        for i in 0..all_results[5].len() {
    
            if all_results[5][i].u8_combo.len() == 2{
                print!("{:#?} ", all_results[5][i].u8_combo);
                print!("{:#?} ", all_results[5][i].best_action);
                print!("{} ", all_results[5][i].best_action_value);
            }
            
        }
        
    
    }

I've thought of using a channel to just send all results to the main thread to combine there but I don't know if it would be any faster. For more information: in each action call, the action is run 10 times. This is to insure that each card combo has each action done to it 1000 times, in order to have somewhat reliable results.

\$\endgroup\$
6
  • 1
    \$\begingroup\$ I think your code has suffered some bad copy-paste errors. For example, this line: if SRR && .test_combo.len() == 2{ looks to be missing something before the . and surrender_result[0] = (result[0]+ action1_result[0]*i as f32)/(i as f32 + 1.0); refers to surrender_result which isn't in the code otherwise. \$\endgroup\$ Sep 5, 2022 at 0:08
  • 7
    \$\begingroup\$ In general, its hard to give you good advice with so much of the code missing. I don't really know what Cards does, but I can tell you need to think its approach to make this code parallelizable. \$\endgroup\$ Sep 5, 2022 at 0:20
  • \$\begingroup\$ Edit : I've fixed the code. \$\endgroup\$
    – PaulEliaz
    Sep 5, 2022 at 11:55
  • \$\begingroup\$ Cards is all of the cards in the deck or decks. It contains a bunch of Card as well as deck properties like the number of cards that are cut from the deck etc... \$\endgroup\$
    – PaulEliaz
    Sep 5, 2022 at 11:56
  • 5
    \$\begingroup\$ that description of Cards is rather vague to the point of being useless. If you want the best help, please share all your code, either in the question or an external link. In this case, it would helpful to run the code myself to see what's slow, but I can't because its missing. Furthermore, given that you don't do anything very complex in the code you've provided, I suspect that Cards must have some really slow code in it. \$\endgroup\$ Sep 5, 2022 at 21:37

1 Answer 1

6
\$\begingroup\$

Your multithreaded code is slow because you use Mutex.

let action1_result = Arc::new(Mutex::new([0.0; 3]));
...        
let handle = thread::spawn(move || {
    let mut action1_result = action1_result.lock().unwrap();
    ...
});

The first thing your threads do is lock your Mutex. Only one thread can lock a Mutex at once. That means that effectively only one thread runs at once. So your code does not run in parallel at all. Its slower because of the overhead of running threads.

What's to be done:

The first thing is that if you want to use a Mutex, lock it as late as possible. You want to do as much calculation as possible first, then lock the Mutex and do the updates. Then you'll do the calculations in parallel up to the point where you have to do the updates which will be sequential.

However, in Rust, its usually better not to lock and update a Mutex. Instead, do your calculations, then return the results in the thread. You can get them by calling .join on the ThreadHandle. Fetch them and do whatever aggregation you need on the result.

However, there is a still better solution in the rayon crate which provides parallel iterators. They allow you to write succinct code that is automatically parallelized. In this case you can do something like:

(0..100).into_par_iter()
    .map(|_| do_some_calculation())
    .fold([0; 3], |accum, result| update(&mut accum, result)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.