On first investigation it appeared that the customer was not using the InitRnd() function to seed the Rnd() generator before asking for the random numbers. This is a documented requirement - Calling InitRnd() is an absolute necessity before using the Rnd() function.
Even so, using InitRnd() as specified in the online help failed to produce sufficiently random results so we decided to take a closer look at the process to see what the problem was and how we could improve it.
Under the hood the Basic+ InitRnd() and Rnd() functions are little more than thin wrappers around the srand() and rand() functions in the Windows C-runtime library. The rand() function actually produces a pseudorandom sequence of numbers, so it is not truly random at all - instead the srand() function is used to seed the starting point for the pseudorandom sequence so it begins in a different place after each initialisation. Also, if srand() is called again with the same seed the same sequence of numbers is generated by rand()!
Going back to the problem, the OpenInsight online help gives an example of using InitRnd() with a seed based on the current time and date (which is what our customer tried next):
0001 /*
0002 The present time and date set the random number
0003 generator with a unique value.
0004 */
0005
0006 InitRnd Time(): Date()
0002 The present time and date set the random number
0003 generator with a unique value.
0004 */
0005
0006 InitRnd Time(): Date()
Unfortunately for this example using the date results in a seed value that doesn't show much variance in it's low order value (it only increments by one per day), and this appears to have the effect of generating a identical Rnd() sequence between each initialisation (especially if called multiple times on the same day which is something that should be avoided). We then thought about swapping the date and time around so that the low order value would hopefully be more varied, but here we find ourselves in danger of generating predictable sequences if we started our application at the same time every day (like at 9:15am for example).
What we needed was some way to fulfil the following criteria:
- Ensure that InitRnd() is only called once per session to avoid the same seed being used more than once.
- Provide InitRnd with a varied seed value that is less predictable and has a greater "spread" than a simple date/time value.
We quickly put together the following function to do this that gave much better results:
0001 subroutine zz_InitRnd( bReset )
0002 /*
0003 Author : Mr C, Sprezzatura Ltd
0004 Date : 29 July 2010
0005 Purpose : Simple function to call initRnd with with a more varied
0006 seed value, and to also ensure we only call it once per
0007 session.
0008
0009 Parameters
0010 ==========
0011
0012 bReset : If TRUE$ then call initRnd regardless of whether or
0013 not it's been called before.
0014
0015 */
0016 declare function getTickCount, getEngineWindow
0017 $insert logical
0018
0019 if assigned( bReset ) else bReset = ""
0020
0021 common /%%_ZZ_INITRND_%%/ zzInitRndSeed@
0022
0023 * // Check to see if we've already been called this session.
0024 if zzInitRndSeed@ then
0025 * // We've called it already - are we asking for a reset?
0026 if bReset else
0027 * // Nope!
0028 return
0029 end
0030 end
0031
0032 * // Start off with the tickcount (ms) and the
0033 * // OE hwnd ...
0034 seed = getTickCount() + getEngineWindow()
0035
0036 * // Now adjust this slightly to help avoid the time
0037 * // factor
0038 if mod( seed, 2 ) then
0039 b = seed[-1,1]
0040 if b then
0041 seed = int( seed / b )
0042 end else
0043 seed = int( seed / 3 )
0044 end
0045 end
0046
0047 * // Right - run it through some more mods to keep it
0048 * // within range of a short int if it's not already...
0049 loop
0050 while ( seed > 0x7FFF )
0051 if mod( seed, 2 ) then
0052 seed -= 0x7FFF
0053 end else
0054 b = seed[-1,1]
0055 if b then
0056 seed = int( seed / b )
0057 end else
0058 seed = int( seed / 3 )
0059 end
0060 end
0061 repeat
0062
0063 initRnd seed
0064
0065 * // Flag that we've already called this...
0066 zzInitRndSeed@ = seed
0067
0068 return
0002 /*
0003 Author : Mr C, Sprezzatura Ltd
0004 Date : 29 July 2010
0005 Purpose : Simple function to call initRnd with with a more varied
0006 seed value, and to also ensure we only call it once per
0007 session.
0008
0009 Parameters
0010 ==========
0011
0012 bReset : If TRUE$ then call initRnd regardless of whether or
0013 not it's been called before.
0014
0015 */
0016 declare function getTickCount, getEngineWindow
0017 $insert logical
0018
0019 if assigned( bReset ) else bReset = ""
0020
0021 common /%%_ZZ_INITRND_%%/ zzInitRndSeed@
0022
0023 * // Check to see if we've already been called this session.
0024 if zzInitRndSeed@ then
0025 * // We've called it already - are we asking for a reset?
0026 if bReset else
0027 * // Nope!
0028 return
0029 end
0030 end
0031
0032 * // Start off with the tickcount (ms) and the
0033 * // OE hwnd ...
0034 seed = getTickCount() + getEngineWindow()
0035
0036 * // Now adjust this slightly to help avoid the time
0037 * // factor
0038 if mod( seed, 2 ) then
0039 b = seed[-1,1]
0040 if b then
0041 seed = int( seed / b )
0042 end else
0043 seed = int( seed / 3 )
0044 end
0045 end
0046
0047 * // Right - run it through some more mods to keep it
0048 * // within range of a short int if it's not already...
0049 loop
0050 while ( seed > 0x7FFF )
0051 if mod( seed, 2 ) then
0052 seed -= 0x7FFF
0053 end else
0054 b = seed[-1,1]
0055 if b then
0056 seed = int( seed / b )
0057 end else
0058 seed = int( seed / 3 )
0059 end
0060 end
0061 repeat
0062
0063 initRnd seed
0064
0065 * // Flag that we've already called this...
0066 zzInitRndSeed@ = seed
0067
0068 return
(Note that we have a global variable in place to ensure that initRnd is only called once per session, unless we explicitly override this)
You can download a text version of zz_InitRnd here.
Disclaimer
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
No comments:
Post a Comment