The last few years I have been creating games for social networks. As a back-end, I use a bunch of Ruby + Sinatra + Redis. Redis is used as the only database. The performance of one Redis database is often not enough, so a cluster of several databases is used. More details about how the solution was created as a cluster of Redis databases can be found in
this article .
Recently, the Go programming language is of great interest to me - too many buns its use is promised to the programmer. It would be desirable to write back-end for new games on it, but the existing and debugged code base on Ruby prevents it.
Therefore, I decided to move in small iterations and started by rewriting the microservices used in Go games.
')
All microservices have a connection to the Redis databases. Usually 8 Redis bases are used, where the game stores data.
The string with the part of the query to Redis is converted using SHA-1 to a hexadecimal number. Then the remainder of dividing this number by 8 is found. And depending on it, the data is written / read from the required database.
The original ruby version, which I decided to rewrite to Go.
def node0(text) return Integer('0x'+Digest::SHA1.hexdigest(text))%8 end
Unfortunately or fortunately, this approach to the forehead on Go did not work. The resulting hexadecimal string was too large a number. This number could not fit into int64 type.
How to solve a problem?
We have 8 bases and a hexadecimal number, from which we must take the remainder of the division by 8.
Answer.
The first thing that comes to mind. We only need to know the last character of the hexadecimal number in order to calculate the remainder of dividing the whole number by 8.
We can easily rewrite the solution in ruby with this in mind:
def node1(text) s = Digest::SHA1.hexdigest(text) return Integer('0x'+s[s.size-1, 1]) % 8 end
The same, but on Go:
func node1 (text string) int64 { h := sha1.New() h.Write([]byte(text)) sha1_hash := hex.EncodeToString(h.Sum(nil)) sha1_hash_len := len(sha1_hash) last := sha1_hash[sha1_hash_len-1:sha1_hash_len] value, _ := strconv.ParseInt(last, 16, 64)
The second thing that turned out to be successfulIn the process of writing the code presented above, it turned out that during the execution of SHA-1, Go represents a number as a set of decimal numbers, and then leads them to a hexadecimal view. Since we need the remainder of the division, we can simply take the last decimal number instead of the hexadecimal last character and save on conversions from one number system to another.
func node2 (text string) int64 { h := sha1.New() h.Write([]byte(text)) mas := h.Sum(nil)
This trick allowed even faster to calculate what is needed. But Ruby failed to do this trick. Therefore, it is more correct to compare the performance of node1 from Ruby and node1 from Go.
And so what is the speed of all presented?How I decided to test:
Calculate which database to put the data related to each of the million users.
On Ruby:
for i in 1..1000000 do node1("user:"+i.to_s) end
and go:
for i := 1; i <= 1000000; i++ { node1("user:"+string(i)) }
The full code can be viewed on
github .
For the purity of the experiment, I ran each script 10 times, and below are the average values.
The code runs on Ruby versions 1.8.7 and 2.1.3. Go version was 1.4.2
Values in seconds. Less is better.
Ruby 1.8.7
node0 - 22.32
node1 - 17.24
node11 - 16.81
Ruby 2.1.3
node0 - 15.46
node1 - 11.15
node11 - 11.05
Go 1.4.2
node1 - 6.15
node2 - 4.36
Why are these old versions of Ruby?I started creating the code for many projects 4-5 years ago. As far as possible, I transferred it to new versions of Ruby. In particular, on 2.1.3. I agree that on the latest version of Ruby 2.2.2 the results may be better, but obviously not twice.
findings- 1. Go 1.4.2 faster than old Ruby versions 3 times. And faster than modern versions 2 times.
- 2. Ruby optimized well from version 1.8.7 to 2.1.3. Speed increase by 25-30%.
- 3. Ruby, you were a true friend and colleague, we went through a lot together, but ... I met Go. Now I'm with him on the way.