2

Let me be the first to say that this isn't something I normally do, but out of curiousity, I'll see if anyone has a good idea on how to handle a problem like this.

The application I am working on is a simulated example of the game Let's make a Deal featuring the Monty Hall problem.

I won't go into details about my implementation, but it more or less allows a user to enter a number of how many games they want to simulate, and then if an option is toggled off, the player of those x games won't switch their choice, while if it is toggled on, they will switch their choice every single instance of the game.

My object generator looks like this:

const game = function(){
    this[0] = null;
    this[1] = null;
    this[2] = null;
    this.pick = Math.floor(Math.random() * 3);
    this.correctpick = Math.floor(Math.random() * 3);
    this[this.correctpick] = 1;
    for (let i=0; i<3; i++){
        if ((this[i] !== 1) && (i !== this.pick)){
            this.eliminated = i;
            break;
        }
    }
}

const games = arg => {
    let ret = [];
    for(let i=0; i<arg; i++){
        ret.push(new game);
    }
    return ret;
}

This structure generates an array which i stringify later that looks like this:

[
  {
    "0": 1,
    "1": null,
    "2": null,
    "pick": 2,
    "correctpick": 0,
    "eliminated": 1
  },
  {
    "0": null,
    "1": null,
    "2": 1,
    "pick": 2,
    "correctpick": 2,
    "eliminated": 0
  }
]

As sloppy as the constructor for game looks, the reason is because I have refactored it into having as few function calls as possible, where now I'm literally only calling Math functions at the current time (I removed any helper functions that made the code easier to read, in opt for performance).

This app can be ran both in the browser and in node (cross platform), but I have clamped the arg a user can pass into the games function to 5 million. Any longer than that and the process (or window) freezes for longer than a few seconds, or even potentially crashes.

Is there anything else I can do to increase performance if a huge number is given by a user? Also, if you need more information, I will be happy to supply it!

Thanks!

simon
  • 744
  • 8
  • 21
  • is there any possibilities to crush their attributes into different arrays/maps? – Nianyi Wang Jan 28 '18 at 10:10
  • 4
    please use not the term **JSON**, if you do not work with strings which are [JSON](http://json.org/) compliant. – Nina Scholz Jan 28 '18 at 10:10
  • 1
    *"This structure generates some json data..."* Expanding on Nina's comment: No, it doesn't. It generates some JavaScript objects. JSON is a *textual notation* for data exchange. [(More here.)](http://stackoverflow.com/a/2904181/157247) If you're dealing with JavaScript source code, and not dealing with a *string*, you're not dealing with JSON. – T.J. Crowder Jan 28 '18 at 10:12
  • Sorry, later on my code I'm using JSON.stringify(simulation) where simulation is that data structure you see above. – simon Jan 28 '18 at 10:13
  • I updated the question. – simon Jan 28 '18 at 10:15
  • `Object.keys(this)[i]` seems to be the buggy and slow version of `this[i]` – Jonas Wilms Jan 28 '18 at 10:17
  • @JonasW. it is required, because I am getting the property name, not the value it contains. If I substitute as you said, it breaks the simulation. – simon Jan 28 '18 at 10:22
  • 1
    @simon why not `i` then??? – Jonas Wilms Jan 28 '18 at 10:27
  • fair point didn't notice this! :) still this does little for performance, but thank you. – simon Jan 28 '18 at 10:29
  • Updated code with your idea, works great. But performance still roughly the same. – simon Jan 28 '18 at 10:30
  • do you need inheritance? if not, you might try parsing a JSON template and customizing any dynamic properties. that _might_ be faster than repeatedly modifying each new object with new properties. i believe every time you add a new prop, V8 has to re-optimize... – dandavis Jan 28 '18 at 10:30
  • with the current structure I dont need inheritance, but I have found that calling game as a direct function, such as game(), was slower than creating a new instance of it using the new keyword. The performance change was seen roughly at > 1 million instances. – simon Jan 28 '18 at 10:33
  • @dandavis "parsing a JSON template" ... so much about performance. I guess `Object.assign({}, template)` is definetly faster – Jonas Wilms Jan 28 '18 at 10:34

2 Answers2

3

The obvious performance optimisation would be not to create and store 5 million objects at all, relieving memory pressure. Instead you'd create the objects on the fly only when you need them and throw them away immediately after. I'm not sure what your app does, but it sounds like you want to re-use the same game instances when evaluating results with the different options. In that case, you need to store them of course - but I'd advise to re-think the design and consider immediately evaluating each game with all possible options, accumulating only the results for each choice of options but not keeping all games in memory.

Apart from that, I'd recommend to simplify a bit:

  • You can drop that loop completely and use some clever arithmetic to find the eliminated option: this.eliminated = this.pick == this.correctpick ? +!this.pick : 3 - this.pick - this.correctpick;. Or use a simple lookup table this.eliminated = [1, 2, 1, 2, 0, 0, 1, 0, 0][this.pick * 3 + this.correctpick].
  • I'd avoid changing the type of the array elements from null (reference) to 1 (number). Just keep them as integers and initialise your elements with 0 instead.
  • Don't store 6 properties in your object that are completely redundant. You only need 2 of them: pick and correctpick - everything else can be computed on the fly from them when you need it. Precomputing and storing it would only be advantageous if the computation was heavy and the result was used often. Neither of this is the case, but keeping a low memory footprint is important (However, don't expect much from this).
Bergi
  • 572,313
  • 128
  • 898
  • 1,281
  • Thank you for your help. I tried changing to the top line you reccomended, and it breaks the sim. It is not doing the same thing. I will however use your idea on setting the number values to 0 as default if that will increase performance. – simon Jan 28 '18 at 11:01
  • Oops, I missed that the `pick` can be the correct one, it's a bit more complicated (but still doesn't need a loop). However, don't the rules of Monty Hall say that the eliminated option must be chosen *at random* if the pick was correct? You are always eliminating the first of the two available options. – Bergi Jan 28 '18 at 11:12
  • i am eliminating the first one the loop finds that isn't picked and also doesn't contain the prize. that is the if conditional if ((this[i] !== 1) && (this.pick !== i)){ – simon Jan 28 '18 at 11:14
  • @simon Yes, and I think "the first one" is wrong. You need to chose a random one that fulfills the condition (there may be 1 or 2 values available to eliminate). – Bergi Jan 28 '18 at 11:15
  • well the only time i'd have to rerandom the removal room would be only in the case if the user got the first choice right, which barely happens (33% of the time). it's been a fun project, and i was skeptical until I saw my code run for the first time against 100+ games, that if you switch, your odds go way up to 66% :) – simon Jan 28 '18 at 11:18
  • @simon Still it does skew the probabilities. And opens new vectors for gaming the system: try to implement a "switch only if something else than 0 was eliminated" mode and check the odds. – Bergi Jan 28 '18 at 11:53
  • It is a bot that is playing the system. The bot doesn't care. It has no idea of where it is placed. It just chooses to either switch its current choice, or not, therefore, I have no need for that use case. If it was humans playing the individual games, you are absolutely correct however. – simon Jan 28 '18 at 11:57
1

Not sure about your implementation, but do you really need an Array?

How about only using results (see snippet)?

If it's blocking the browser that worries you, maybe delegating the work to a web worker is the solution for that: see this jsFiddle for a web worker version of this snippet.

(() => {
  document.querySelector("#doit")
   .addEventListener("click", playMontyHall().handleRequest);

  function playMontyHall() {
    const result = document.querySelector("#result");
    const timing = document.querySelector("#timing");
    const nOfGames = document.querySelector("#nGames");
    const switchDoors = document.querySelector("#switchyn");

    // Create a game
    const game = (doSwitch) => {
      const doors = [0, 1, 2];
      const pick = Math.floor(Math.random() * 3);
      const correctPick = Math.floor(Math.random() * 3);
      const eliminated = doors.filter(v => v !== pick && v !== correctPick)[0];

      return {
        correctpick: correctPick,
        pick: doSwitch ? doors.filter(v => v !== pick && v !== eliminated)[0] : pick,
        eliminated: eliminated,
      };
    };
    
    const getWinner = game  => ~~(game.correctpick === game.pick);
    
    // Sum  wins using a generator function
    const winningGenerator = function* (doSwitch, n) {
      let wins = 0;
      
      while (n--) {
        wins += getWinner(game(doSwitch));
        yield wins;
      }
    };

    // calculate the number of succeeded games
    const calculateGames = (nGames, switchAlways) => {
      const funNGames = winningGenerator(switchAlways, nGames);
      let numberOfWins = 0;
      
      while (nGames--) {
        numberOfWins = funNGames.next().value;
      }
      
      return numberOfWins;
    }
    
    const cleanUp = playOut => {
     result.textContent =
        "Playing ... (it may last a few seconds)";
      timing.textContent = "";
      setTimeout(playOut, 0);
    };
    
    const report = results => {
     timing.textContent = `This took ${
         (performance.now() - results.startTime).toFixed(3)} milliseconds`;
      result.innerHTML =
         `<b>${!results.switchAlways ? "Never s" : "Always s"}witching doors</b>:
         ${results.winners} winners out of ${results.nGames} games
         (${((results.winners/+results.nGames)*100).toFixed(2)}%)`;
    };
    
    // (public) handle button click
    function clickHandle() {
     
      cleanUp(() => {
        const nGames = nOfGames.value || 5000000;
        const switchAlways = switchDoors.checked;
        report({
          switchAlways: switchAlways,
         startTime: performance.now(),
          winners: calculateGames(nGames, switchAlways),
          nGames: nGames
        });
      });
      
    }

    return {
      handleRequest: clickHandle
    };
  }

})();
body {
  margin: 2em;
  font: normal 12px/15px verdana, arial;
}

#timing {
  color: red;
}
<p><input type="number" id="nGames" value="5000000"> N of games</p>
<p><input type="checkbox" id="switchyn"> Always switch doors</p>
<p><button id="doit">Play</button>
<p id="result"></p>
<p id="timing"></p>
KooiInc
  • 112,400
  • 31
  • 139
  • 174
  • This is an amazing solution! I will take a look at your code thank you! – simon Jan 28 '18 at 20:34
  • I changed my answer to yours. You have some incredible stuff in here Kooilnc, thank you! – simon Jan 29 '18 at 15:33
  • Ok, you've got me hooked on web / service workers now. Thank you so much. I have but one simple question for you. What is the point of your setTimeout(playout, 0) in cleanup() ? Only thing I do not fully understand in your code. Once again THANK you! :) – simon Jan 30 '18 at 02:04
  • Hi @simon, glad I could help. The point of `setTimeout` is to be able to clean up the screen. Without it, the UI rewrites _after_ completing the playing. It's a bit of a trick. – KooiInc Jan 30 '18 at 09:03
  • Addendum: `setTimeout` is not necessary in the web worker code: you can use the worker messaging handler to clean up the screen (because the worker is a different thread). I adjusted the jsfiddle code. – KooiInc Jan 30 '18 at 09:08
  • @Kooilnc I see. I fixed up some of your code that I borrowed from you to suit my own needs but this is without a doubt the accepted answer. One final (I Promise!) question: Worker or blob is not native in node. Is there a way to utilize the same sort of behavior in node? Thanks!!! – simon Jan 30 '18 at 14:05
  • Hi @simon, given the answers @ https://stackoverflow.com/questions/10773564/which-would-be-better-for-concurrent-tasks-on-node-js-fibers-web-workers-or-t I don't think trying to implement web workers in nodejs is a smart thing. Maybe creating an extra server on a different port to handle the workload, or using something like `child_process.spawn` would be an idea. If the result of your nodejs application ends up in a browser, the app can be seen as the actual worker I suppose. – KooiInc Jan 30 '18 at 15:47