讓我們看看你有多瞭解你的電腦!所有這些程式裡都包含一個 NUMBER 變數。你的任務是:猜猜需要把 NUMBER 設定為一個多大的數,才能讓相應的程式執行耗時一秒。
你不需要猜出準確數字:它們都介於1到十億之間。猜出數量級即可!注意以下幾點:
- 如果正確答案是 38,000,那麼猜 10,000 和 100,000 都是正確的。
- 我們知道,計算機有著不同的硬碟效能、網速和CPU速度!我們試圖讓你能夠區分執行 10次/秒 和 10萬次/秒 的程式碼之間的差別。一臺新的電腦並不能讓你的程式碼執行速度快上1000倍 :)
- 也就是說,所有的程式碼執行在一臺新款膝上型電腦上,它有著快速的SSD硬碟以及還不錯的網速。C 程式碼全部使用 gcc -O2 來進行編譯
祝你好運!很多問題的答案會出乎你的意料。我們會匿名收集你的答案,未來我們公佈一些圖表哦,敬請期待! =D
歡迎來到第一個問題!這個問題是讓你練練手:
在一秒中之內能執行多少次迴圈?(可能比你想象的要多得多哦!)
猜猜看:1 秒鐘執行迴圈次數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdlib.h> // 猜數字: 在1秒鐘時間內 // 這個迴圈可以執行多少次 int main(int argc, char **argv) { int NUMBER, i, s; NUMBER = atoi(argv[1]); for (s = i = 0; i < NUMBER; ++i) { s += 1; } return 0; } |
準確答案:550,000,000
猜猜看:1秒鐘執行迴圈次數
1 2 3 4 5 6 7 8 9 10 11 |
#!/usr/bin/env python # 猜數字: 一秒鐘內可以執行 # 多少次空迴圈 def f(NUMBER): for _ in xrange(NUMBER): pass import sys f(int(sys.argv[1])) |
準確答案:68,000,000
既然我們已經知道了 Python 的極限(1億 指令/秒),讓我們看一個更加實際的例子。字典在Python中的應用隨處可見,所以,在一秒鐘的時間裡,我們能夠向一個字典新增多少元素呢?
猜猜看:1秒鐘執行迴圈次數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#!/usr/bin/env python # 猜數字: 在一秒鐘內,我們能向 # 字典新增多少個條目? # 注意: 我們使用 `i % 1000` # 來控制字典的大小 def f(NUMBER): d = {} for i in xrange(NUMBER): d[i % 1000] = i import sys f(int(sys.argv[1])) |
準確答案:11,000,000
當你搞定這題之後,讓我們看一個更復雜的操作,用 Python 內建的 HTTP 請求解析器來解析一個請求
猜猜看:1秒鐘可以解析的HTTP請求數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#!/usr/bin/env python # 猜數字: 一秒鐘可以解析多少HTTP請求 from BaseHTTPServer import BaseHTTPRequestHandler from StringIO import StringIO class HTTPRequest(BaseHTTPRequestHandler): def __init__(self, request_text): self.rfile = StringIO(request_text) self.raw_requestline = self.rfile.readline() self.error_code = self.error_message = None self.parse_request() def send_error(self, code, message): self.error_code = code self.error_message = message request_text = """GET / HTTP/1.1 Host: localhost:8001 Connection: keep-alive Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-GB,en-US;q=0.8,en;q=0.6 """ def f(NUMBER): for _ in range(NUMBER): HTTPRequest(request_text) import sys f(int(sys.argv[1])) |
接下來,我們將會看到,下載一個網頁 vs 執行一個 Python 指令碼!
提示:本題答案都小於1億 :)
猜猜看:1秒鐘可以完成的HTTP請求數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#!/usr/bin/env python # 猜數字: 一秒鐘的時間,我們可以從 # google.com 下載多少頁面? from urllib2 import urlopen def f(NUMBER): for _ in xrange(NUMBER): r = urlopen("http://google.com") r.read() import sys f(int(sys.argv[1])) |
準確答案:4
猜猜看:1秒鐘執行迴圈次數
1 2 3 4 5 6 7 8 9 10 |
#!/bin/bash # 猜數字: 在一秒內,我們可以啟動多少次 # Python直譯器? NUMBER=$1 for i in $(seq $NUMBER); do python -c ''; done |
準確答案:77
啟動程式本身就非常耗時,並不只有是Python是這樣。如果我們只是執行/bin/true,那麼1秒能做500次,所以看起來執行任何程式一般需要大約1毫秒時間。當然,下載網頁的快慢很大程度上取決於網頁大小、網路連線速度、以及伺服器間的距離,今天我們並不會深入探討網路效能(網路效能是一件非常有趣的事情)。一個從事高效能網路開發的朋友告訴我,一次網路往返可以做到250ns(!!!),但是要求計算機距離非常近,同時搭配豪華的硬體配置。對我們和Google來講,耗時是它的一百萬倍。在一個納秒的時間,光只能傳播一英尺,而谷歌的伺服器遠在250英尺以外的地方。
在一秒鐘時間內,可以向硬碟寫多少位元組的資料?我們都知道向記憶體寫資料更快些,但是快多少呢?下面的程式碼在一個裝有SSD的硬碟上執行。
猜猜看:一秒鐘寫入多少位元組
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#!/usr/bin/env python # 猜數字: 一秒的時間我們可以向一個檔案寫入 # 多少byt位元組? # 注意:我們確保所有資料在退出前都已經同步到硬碟 import tempfile import os CHUNK_SIZE = 1000000 s = "a" * CHUNK_SIZE def cleanup(f, name): f.flush() os.fsync(f.fileno()) f.close() try: os.remove(name) except: pass def f(NUMBER): name = './out' f = open(name, 'w') bytes_written = 0 while bytes_written < NUMBER: f.write(s) bytes_written += CHUNK_SIZE cleanup(f, name) import sys f(int(sys.argv[1])) |
準確答案:342,000,000
猜猜看:一秒鐘寫入多少位元組
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#!/usr/bin/env python # 猜數字: 在一秒鐘內,我們能夠向一個 # 記憶體中的字串寫入多少位元組 import cStringIO CHUNK_SIZE = 1000000 s = "a" * CHUNK_SIZE def f(NUMBER): output = cStringIO.StringIO() bytes_written = 0 while bytes_written < NUMBER: output.write(s) bytes_written += CHUNK_SIZE import sys f(int(sys.argv[1])) |
準確答案:2,000,000,000
硬碟比記憶體要慢,即使你使用“較慢”的語言,比如Python,這種差別也是有影響的。如果你使用一個非常快速的硬碟(我的SSD已知的寫入速度>500MB/s,可以稱得上快)很多事情最終都受限於硬碟的速度。我們來看下一個例子!
檔案時間到!有時候我執行一條 grep 命令處理大量資料,然後它就一直執行下去了,grep 在一秒內可以搜尋多少位元組的資料呢?
注意,當程式執行時,grep讀入的資料已經全部讀入記憶體。這讓我們能夠知道grep慢的原因,多少是因為搜尋,多少是因為讀取到硬碟。
列出檔案同樣耗時!在一秒鐘內可以列出多少檔案呢?
猜猜看:1秒能夠搜尋多少位元組?
1 2 3 4 5 6 7 8 9 |
#!/bin/bash # 猜數字: `grep`命令1秒能夠搜尋多少位元組 # 注意: 資料已經在記憶體中 NUMBER=$1 cat /dev/zero | head -c $NUMBER | grep blah exit 0 |
準確答案:2,000,000,000
猜猜看:一秒能夠列出多少檔案?
1 2 3 4 5 6 |
#!/bin/bash # Number to guess: `find`命令 一秒鐘能夠列出多少檔案? # 注意: 檔案在檔案系統快取中。 find / -name '*' 2> /dev/null | head -n $1 > /dev/null |
準確答案:325,000
很好!現在我知道grep可以以2GB/s的速度搜尋,所以,至少在這個例子中,我們程式的速度主要受限於硬碟速度而不是grep的速度。
序列化通常是一個很耗時的工作,尤其是當你需要反覆的序列化/反序列化一份資料的時候,真的非常痛苦。這裡有一些基準測試:解析 64K 的JSON檔案,同樣的資料用 msgpack 格式編碼。
猜猜看:一秒鐘迴圈次數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#!/usr/bin/env python # 猜數字: 在一秒鐘內,我們能夠解析一個 # 64K 的 JSON 檔案多少次? import json with open('./setup/protobuf/message.json') as f: message = f.read() def f(NUMBER): for _ in xrange(NUMBER): json.loads(message) import sys f(int(sys.argv[1])) |
準確答案:449
猜猜看:一秒鐘迴圈次數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#!/usr/bin/env python # 猜數字: 在一秒鐘內,我們能夠解析一個 # 46K 的msgpack 資料多少次? import msgpack with open('./setup/protobuf/message.msgpack') as f: message = f.read() def f(NUMBER): for _ in xrange(NUMBER): msgpack.unpackb(message) import sys f(int(sys.argv[1])) |
準確答案:4000
基本上任何一個談論序列化的人都會提到 capnproto 可以進行即時的序列化。我們只是想讓你明白,反序列化一個64K的資料需要花上1微妙(據我們所知,這是非常長的時間了),而且你選擇的格式和庫也會帶來非常大的影響。
資料庫。我們並沒有為你準備炫酷的PostgreSQL,取而代之的是我們弄了兩份包含一千萬行資料的SQLite資料表,一份設定了索引,另一份沒有。
猜猜看:一秒鐘執行的查詢次數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#!/usr/bin/env python # 猜猜看: 一秒鐘的時間,我們可以從一 # 個包含一千萬行資料,並設定了 # 索引的表中選取多少行 import sqlite3 conn = sqlite3.connect('./indexed_db.sqlite') c = conn.cursor() def f(NUMBER): query = "select * from my_table where key = %d" % 5 for i in xrange(NUMBER): c.execute(query) c.fetchall() import sys f(int(sys.argv[1])) |
準確答案:53000
猜猜看:一秒鐘執行的查詢次數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#!/usr/bin/env python # 猜猜看: 一秒鐘的時間,我們可以從一 # 個包含一千萬行資料,且沒有設定 # 索引的表中選取多少行 import sqlite3 conn = sqlite3.connect('./unindexed_db.sqlite') c = conn.cursor() def f(NUMBER): query = "select * from my_table where key = %d" % 5 for i in xrange(NUMBER): c.execute(query) c.fetchall() import sys f(int(sys.argv[1])) |
準確答案:2
並不出乎我們的意料:索引的效果很贊。20幾微秒進行一次帶索引查詢意味著,如果這是基於一個距離很遠的資料伺服器的連線,那麼查詢的時間主要受限於到伺服器的網路往返時間。
下面到雜湊時間啦!在這裡,我們將比較MD5(設計的初衷就是要速度快)和 bcrypt(設計的初衷就是要速度慢)。用MD5你在1秒時間內可以雜湊到相當多的東西,而用 bcrypt 則不能。
猜猜看:一秒內可以雜湊多少位元組的資料
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#!/usr/bin/env python # 猜數字: 用MD5sum一秒內可以處理多少位元組的資料 import hashlib CHUNK_SIZE = 10000 s = 'a' * CHUNK_SIZE def f(NUMBER): bytes_hashed = 0 h = hashlib.md5() while bytes_hashed < NUMBER: h.update(s) bytes_hashed += CHUNK_SIZE h.digest() import sys f(int(sys.argv[1])) |
準確答案:455,000,000
猜猜看:一秒內可以雜湊多少位元組的密碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#!/usr/bin/env python # 猜數字: 使用bcrypt一秒內可以雜湊多少位元組的密碼 import bcrypt password = 'a' * 100 def f(NUMBER): for _ in xrange(NUMBER): bcrypt.hashpw(password, bcrypt.gensalt()) import sys f(int(sys.argv[1])) |
準確答案:3
接下來,讓我們探討一下記憶體訪問。現在的 CPU 有 L1 和 L2 快取,這比主記憶體訪問速度更快。這意味著,循序訪問記憶體(CPU可以把一大塊資料載入進快取)通常比不按順序訪問記憶體能提供更快的程式碼。
讓我們看看,事實是多麼令我們吃驚吧!你可能需要參考《Latency Numbers Every Programmer Should Know》來猜這一題。
猜猜看:一秒鐘寫的位元組數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <stdlib.h> #include <stdio.h> // 猜數字:一秒內我們可以分配 // 並填充一塊多大的陣列? // 我們故意讓它這麼複雜,其實沒有必要,這樣才能和無序訪問進行對比 :) int main(int argc, char **argv) { int NUMBER, i; NUMBER = atoi(argv[1]); char* array = malloc(NUMBER); int j = 1; for (i = 0; i < NUMBER; ++i) { j = j * 2; if (j > NUMBER) { j = j - NUMBER; } array[i] = j; } printf("%d", array[NUMBER / 7]); // so that -O2 doesn't optimize out the loop return 0; } |
準確答案:376,000,000
猜猜看:一秒鐘寫的位元組數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <stdlib.h> #include <stdio.h> // 猜數字:一秒內我們可以分配 // 並填充一塊多大的陣列? // 使用無序訪問而不是有序訪問 int main(int argc, char **argv) { int NUMBER, i; NUMBER = atoi(argv[1]); char* array = malloc(NUMBER); int j = 1; for (i = 0; i < NUMBER; ++i) { j = j * 2; if (j > NUMBER) { j = j - NUMBER; } array[j] = j; } printf("%d", array[NUMBER / 7]); // so that -O2 doesn't optimize out the loop return 0; } |
準確答案:68,000,000
我們通常不會寫太多的C程式碼,所以並不會總是受其影響。但是如果你很在意你的命令耗時多少微秒的時候(當你嘗試每秒處理十億資料的時候,你就會在意),你就會很在意此類事情了。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式