Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize MakeAlphaString #295

Merged
merged 1 commit into from
Jan 12, 2022

Conversation

JelteF
Copy link
Contributor

@JelteF JelteF commented Dec 13, 2021

When building the dataset I was able to max out the CPU on the
node that's running HammerDB (as opposed to the database that this node
is connecting to). Since generating data takes a significant amount of
time I took some time trying to find some low hanging fruit for
optimization.

After some profiling, by far the most CPU heavy function turned out to
be MakeAlphaString. After digging down even further it turns out that
the thing that makes this function slow is the amount of rand calls it
does. Since we cannot completely remove this rand call, I investigated
some ways of reducing the number of calls.

The new implementation is able to generate a sequence of 4 random
characters with a single rand call. This is done in two ways:

  1. Pre-compute an array of all combinations of two alphanumeric characters.
  2. Use a single random number to generate two random numbers by creating
    a single random number in the range of 0-N^2. Then two get two
    independent random numbers out of that, you can divide by N. Then use
    the result of this division as the first random number and the remainder
    as the second random number.

The result of these changes is that generating a random string with a
length between 300 and 500 characters now only takes ~43% of the time it
did before (on my machine).

Apart from I tried some more approaches, but this turned out to run the
fastest on my machine. I used the script at the end of this commit
message to compare different approaches (including the simple approach).
Please run the benchmark on your own machine.

One thing to note is that the suggested MakeAlphaString implementation
ignores the 3rd and 4th argument, and instead always uses all
alphanumeric ASCII characters. Looking at the existing code there's only
one place where a different character set is used. This is in
tpchcommon-1.0.tm, which has its own MakeAlphaString implementation.
All other uses use the same alphanumeric ASCII character array. Before
merging this it would be good to remove the ignored1 and ignored2
arguments from the new MakeAlphaString implementation and all its
call sites. I did not do that so far so first there could be some discussion if
this faster implementation makes sense.

package require Tclx
namespace eval benchutils {
    namespace export alphanumArray alphanumLength MakeAlphaString MakeAlphaStringFast MakeAlphaStringFastest
proc RandomNumber {m M} {return [expr {int($m+rand()*($M+1-$m))}]}

set alphanumArray [ list 0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z ]
set alphanumLength [ llength $alphanumArray ]

proc MakeAlphaString { x y chArray chalen } {
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i} {
        append alphastring [lindex $chArray [ expr {int(rand()*$chalen)}]]
    }
    return $alphastring
}

proc CreateListProduct2 { chArray chalen } {
    for {set i 0} {$i < $chalen} {incr i} {
        set char1 [ lindex $chArray $i]
        for {set j 0} {$j < $chalen } {incr j } {
            set char2 [ lindex $chArray $j]
            set combined $char1
            append combined $char2
            lappend products $combined
        }
    }
    return $products
}
set alphanum2Array [CreateListProduct2 $alphanumArray $alphanumLength ]
set alphanum2Length [ llength $alphanum2Array ]

proc MakeAlphaStringFast { x y ignore1 ignore2 } {
    variable alphanum2Array;
    variable alphanum2Length;
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i 2} {
        set randnum [ expr {int(rand() * $alphanum2Length)} ]
        append alphastring [lindex $alphanum2Array $randnum ]
    }
    return [ string range $alphastring 0 $len-1 ]
}

set alphanum2LengthSquared [ expr { $alphanum2Length * $alphanum2Length } ]

proc MakeAlphaStringFastest { x y ignore1 ignore2} {
    variable alphanum2Array;
    variable alphanum2Length;
    variable alphanum2LengthSquared;
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i 4} {
        set randnumFull [ expr {int(rand() * $alphanum2LengthSquared)} ]
        set randnum1 [ expr {$randnumFull / $alphanum2Length} ]
        set randnum2 [ expr {$randnumFull % $alphanum2Length} ]
        append alphastring [lindex $alphanum2Array $randnum1 ]
        append alphastring [lindex $alphanum2Array $randnum2 ]
    }
    return [ string range $alphastring 0 $len-1 ]
}

proc CreateListProduct3 { chArray chalen } {
    for {set i 0} {$i < $chalen } {incr i } {
        set char1 [ lindex $chArray $i]
        for {set j 0} {$j < $chalen } {incr j } {
            set char2 [ lindex $chArray $j]
            for {set k 0} {$k < $chalen } {incr k } {
                set char3 [ lindex $chArray $k ]
                set combined $char1
                append combined $char2 $char3
                lappend products $combined
            }
        }
    }
    return $products
}

set alphanum3Array [CreateListProduct3 $alphanumArray $alphanumLength ]
set alphanum3Length [ llength $alphanum3Array ]
set alphanum3LengthSquared [ expr { $alphanum3Length * $alphanum3Length } ]

proc MakeAlphaStringMedium { x y ignore1 ignore2} {
    variable alphanum3Array;
    variable alphanum3Length;
    variable alphanum3LengthSquared;
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i 6} {
        set randnumFull [ expr {int(rand() * $alphanum3LengthSquared)} ]
        set randnum1 [ expr {$randnumFull / $alphanum3Length} ]
        set randnum2 [ expr {$randnumFull % $alphanum3Length} ]
        append alphastring [lindex $alphanum3Array $randnum1 ]
        append alphastring [lindex $alphanum3Array $randnum2 ]
    }
    return [ string range $alphastring 0 $len-1 ]
}

proc MakeAlphaStringMedium2 { x y ignore1 ignore2 } {
    variable alphanum3Array;
    variable alphanum3Length;
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i 3} {
        set randnum [ expr {int(rand() * $alphanum3Length)} ]
        append alphastring [lindex $alphanum3Array $randnum ]
    }
    return [ string range $alphastring 0 $len-1 ]
}

}
puts [time {benchutils::MakeAlphaString 300 500 $alphanumArray $alphanumLength } 100000]
puts [time {benchutils::MakeAlphaStringFast 300 500 0 0 } 100000]
puts [time {benchutils::MakeAlphaStringFastest 300 500 0 0 } 100000]
puts [time {benchutils::MakeAlphaStringMedium 300 500 0 0 } 100000]
puts [time {benchutils::MakeAlphaStringMedium2 300 500 0 0 } 100000]

Timing results of this benchmark script on my machine:

72.20168 microseconds per iteration
39.30159 microseconds per iteration
31.65163 microseconds per iteration
51.54411 microseconds per iteration
56.4996 microseconds per iteration

When building the dataset I was able to max out the CPU on the
node that's running HammerDB (as opposed to the database that this node
is connecting to). Since generating data takes a significant amount of
time I took some time trying to find some low hanging fruit for
optimization.

After some profiling, by far the most CPU heavy function turned out to
be `MakeAlphaString`. After digging down even further it turns out that
the thing that makes this function slow is the amount of `rand` calls it
does. Since we cannot completely remove this `rand` call, I investigated
some ways of reducing the number of calls.

The new implementation is able to generate a sequence of 4 random
characters with a single `rand` call. This is done in two ways:
1. Pre-compute an array of all combinations of two alphanumeric characters.
2. Use a single random number to generate two random numbers by creating
   a single random number in the range of 0-N^2. Then two get two
   independent random numbers out of that, you can divide by N. Then use
   the result of this division as the first random number and the remainder
   as the second random number.

The result of these changes is that generating a random string with a
length between 300 and 500 characters now only takes ~43% of the time it
did before (on my machine).

Apart from I tried some more approaches, but this turned out to run the
fastest on my machine. I used the script at the end of this commit
message to compare different approaches (including the simple approach).
Please run the benchmark on your own machine.

One thing to note is that the suggested `MakeAlphaString` implementation
ignores the 3rd and 4th argument, and instead always uses all
alphanumeric ASCII characters. Looking at the existing code there's only
one place where a different character set is used. This is in
`tpchcommon-1.0.tm`, which has its own `MakeAlphaString` implementation.
All other uses use the same alphanumeric ASCII character array. Before
merging this it would be good to remove the `ignored1` and `ignored2`
arguments from the new `MakeAlphaString` implementation. I did not do
that so far so first there could be some discussion if this
faster implementation makes sense.

```
package require Tclx
namespace eval benchutils {
    namespace export alphanumArray alphanumLength MakeAlphaString MakeAlphaStringFast MakeAlphaStringFastest
proc RandomNumber {m M} {return [expr {int($m+rand()*($M+1-$m))}]}

set alphanumArray [ list 0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z ]
set alphanumLength [ llength $alphanumArray ]

proc MakeAlphaString { x y chArray chalen } {
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i} {
        append alphastring [lindex $chArray [ expr {int(rand()*$chalen)}]]
    }
    return $alphastring
}

proc CreateListProduct2 { chArray chalen } {
    for {set i 0} {$i < $chalen} {incr i} {
        set char1 [ lindex $chArray $i]
        for {set j 0} {$j < $chalen } {incr j } {
            set char2 [ lindex $chArray $j]
            set combined $char1
            append combined $char2
            lappend products $combined
        }
    }
    return $products
}
set alphanum2Array [CreateListProduct2 $alphanumArray $alphanumLength ]
set alphanum2Length [ llength $alphanum2Array ]

proc MakeAlphaStringFast { x y ignore1 ignore2 } {
    variable alphanum2Array;
    variable alphanum2Length;
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i 2} {
        set randnum [ expr {int(rand() * $alphanum2Length)} ]
        append alphastring [lindex $alphanum2Array $randnum ]
    }
    return [ string range $alphastring 0 $len-1 ]
}

set alphanum2LengthSquared [ expr { $alphanum2Length * $alphanum2Length } ]

proc MakeAlphaStringFastest { x y ignore1 ignore2} {
    variable alphanum2Array;
    variable alphanum2Length;
    variable alphanum2LengthSquared;
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i 4} {
        set randnumFull [ expr {int(rand() * $alphanum2LengthSquared)} ]
        set randnum1 [ expr {$randnumFull / $alphanum2Length} ]
        set randnum2 [ expr {$randnumFull % $alphanum2Length} ]
        append alphastring [lindex $alphanum2Array $randnum1 ]
        append alphastring [lindex $alphanum2Array $randnum2 ]
    }
    return [ string range $alphastring 0 $len-1 ]
}

proc CreateListProduct3 { chArray chalen } {
    for {set i 0} {$i < $chalen } {incr i } {
        set char1 [ lindex $chArray $i]
        for {set j 0} {$j < $chalen } {incr j } {
            set char2 [ lindex $chArray $j]
            for {set k 0} {$k < $chalen } {incr k } {
                set char3 [ lindex $chArray $k ]
                set combined $char1
                append combined $char2 $char3
                lappend products $combined
            }
        }
    }
    return $products
}

set alphanum3Array [CreateListProduct3 $alphanumArray $alphanumLength ]
set alphanum3Length [ llength $alphanum3Array ]
set alphanum3LengthSquared [ expr { $alphanum3Length * $alphanum3Length } ]

proc MakeAlphaStringMedium { x y ignore1 ignore2} {
    variable alphanum3Array;
    variable alphanum3Length;
    variable alphanum3LengthSquared;
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i 6} {
        set randnumFull [ expr {int(rand() * $alphanum3LengthSquared)} ]
        set randnum1 [ expr {$randnumFull / $alphanum3Length} ]
        set randnum2 [ expr {$randnumFull % $alphanum3Length} ]
        append alphastring [lindex $alphanum3Array $randnum1 ]
        append alphastring [lindex $alphanum3Array $randnum2 ]
    }
    return [ string range $alphastring 0 $len-1 ]
}

proc MakeAlphaStringMedium2 { x y ignore1 ignore2 } {
    variable alphanum3Array;
    variable alphanum3Length;
    set len [ RandomNumber $x $y ]
    for {set i 0} {$i < $len} {incr i 3} {
        set randnum [ expr {int(rand() * $alphanum3Length)} ]
        append alphastring [lindex $alphanum3Array $randnum ]
    }
    return [ string range $alphastring 0 $len-1 ]
}

}
puts [time {benchutils::MakeAlphaString 300 500 $alphanumArray $alphanumLength } 100000]
puts [time {benchutils::MakeAlphaStringFast 300 500 0 0 } 100000]
puts [time {benchutils::MakeAlphaStringFastest 300 500 0 0 } 100000]
puts [time {benchutils::MakeAlphaStringMedium 300 500 0 0 } 100000]
puts [time {benchutils::MakeAlphaStringMedium2 300 500 0 0 } 100000]
```

Timing results of this benchmark script on my machine:
```
72.20168 microseconds per iteration
39.30159 microseconds per iteration
31.65163 microseconds per iteration
51.54411 microseconds per iteration
56.4996 microseconds per iteration
```
@sm-shaw
Copy link
Contributor

sm-shaw commented Dec 13, 2021

This is a great suggestion and if performance can be improved, here definitely something that should be adopted. I will test it out.

@sm-shaw
Copy link
Contributor

sm-shaw commented Jan 7, 2022

I have tested this out for server builds using MariaDB and PostgreSQL with the following results:
TPROC-C 800WH with 64VUs

postgres orig = 12 min 40 secs / postgres new = 8 min 54 secs = 30% improvement
mariadb orig = 20 min 46 secs / mariadb new = 19 min 5 secs = 8% improvement

TPROC-H SF10 with 64VUs
postgres orig = 3 min 43 secs / postgres new = 3 min 52 secs

For the environments tested, it gives very good improvement for TPROC-C, TPROC-H has no impact, however this is likely because MakeAlphaString is not the slowest function as there are additional grammar rules in building the text strings.

I have also tested on a PC with SQL Server and this shows similar results, with the TPROC-C build being faster and the TPROC-H build similar.

I have also tested performance with both builds and have not detected any differences between them.

So, yes, this looks good and the recommendation to the subcommittee is that we accept the pull request.
For now, I think it makes sense to leave the ignored1 and ignored2 arguments in place so where MakeAlphaString is called is unaffected. If no issues are raised about the change, then these can be removed at a future release without affecting functionality.

sm-shaw added a commit that referenced this pull request Jan 12, 2022
@sm-shaw sm-shaw merged commit bea1552 into TPC-Council:master Jan 12, 2022
@sm-shaw
Copy link
Contributor

sm-shaw commented Jan 12, 2022

Merging Pull Request as voted on by TPC-OSS subcommittee on 11th Jan 2022 with manual resolve of conflicts.
commit bea1552 also reformats the source, so some changes are whitespace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants