专栏名称: Python开发者
人生苦短,我用 Python。伯乐在线旗下账号「Python开发者」分享 Python 相关的技术文章、工具资源、精选课程、热点资讯等。
目录
相关文章推荐
Python爱好者社区  ·  一个薪资被严重低估的方向,很稳... ·  2 天前  
Python爱好者社区  ·  这估计是双非高校被黑的最惨的一次。获得杰青的 ... ·  3 天前  
Python爱好者社区  ·  全球第二大成人网站、“Web世界的最后捍卫者 ... ·  1 周前  
Python爱好者社区  ·  国庆宅家做私活,赚了! ·  1 周前  
51好读  ›  专栏  ›  Python开发者

你知道计算机在一秒内可以做多少事情吗?

Python开发者  · 公众号  · Python  · 2017-01-31 19:15

正文

(点击上方蓝字,快速关注我们)


译文:伯乐在线 -  艾凌风

英文:computers-are-fast.github.io

如有好文章投稿,请点击 → 这里了解详情

如需转载,发送「转载」二字查看说明


让我们看看你有多了解你的电脑!所有这些程序里都包含一个 NUMBER 变量。你的任务是:猜猜需要把 NUMBER 设置为一个多大的数,才能让相应的程序执行耗时一秒。



你不需要猜出准确数字:它们都介于1到十亿之间。猜出数量级即可!注意以下几点:


  • 如果正确答案是 38,000,那么猜 10,000 和 100,000 都是正确的。

  • 我们知道,计算机有着不同的硬盘性能、网速和CPU速度!我们试图让你能够区分运行 10次/秒 和 10万次/秒 的代码之间的差别。一台新的电脑并不能让你的代码运行速度快上1000倍 :)

  • 也就是说,所有的代码运行在一台新款笔记本电脑上,它有着快速的SSD硬盘以及还不错的网速。C 代码全部使用 gcc -O2 来进行编译


祝你好运!很多问题的答案会出乎你的意料。我们会匿名收集你的答案,未来我们公布一些图表哦,敬请期待! =D




欢迎来到第一个问题!这个问题是让你练练手:


在一秒中之内能执行多少次循环?(可能比你想象的要多得多哦!)


猜猜看:1 秒钟执行循环次数


#include

 

// 猜数字: 在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秒钟执行循环次数


#!/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秒钟执行循环次数


#!/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请求数


#!/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请求数


#!/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秒钟执行循环次数


#!/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的硬盘上执行。


猜猜看:一秒钟写入多少字节


#!/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


猜猜看:一秒钟写入多少字节


#!/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秒能够搜索多少字节?


#!/bin/bash

 

# 猜数字: `grep`命令1秒能够搜索多少字节

# 注意: 数据已经在内存中

 

NUMBER=$1

 

cat /dev/zero | head -c $NUMBER | grep blah

exit 0


准确答案:2,000,000,000


猜猜看:一秒能够列出多少文件?


#!/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 格式编码。


猜猜看:一秒钟循环次数


#!/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


猜猜看:一秒钟循环次数


#!/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数据表,一份设置了索引,另一份没有。


猜猜看:一秒钟执行的查询次数


#!/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


猜猜看:一秒钟执行的查询次数


#!/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


并不出乎我们的意料:索引的效果很zan。20几微秒进行一次带索引查询意味着,如果这是基于一个距离很远的数据服务器的连接,那么查询的时间主要受限于到服务器的网络往返时间。


下面到哈希时间啦!在这里,我们将比较MD5(设计的初衷就是要速度快)和 bcrypt(设计的初衷就是要速度慢)。用MD5你在1秒时间内可以哈希到相当多的东西,而用 bcrypt 则不能。


猜猜看:一秒内可以哈希多少字节的数据


#!/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


猜猜看:一秒内可以哈希多少字节的密码


#!/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》来猜这一题。


猜猜看:一秒钟写的字节数


#include

#include

 

// 猜数字:一秒内我们可以分配

// 并填充一块多大的数组?

 

// 我们故意让它这么复杂,其实没有必要,这样才能和无序访问进行对比 :)

 

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


猜猜看:一秒钟写的字节数


#include

#include

 

// 猜数字:一秒内我们可以分配

// 并填充一块多大的数组?

 

// 使用无序访问而不是有序访问

 

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代码,所以并不会总是受其影响。但是如果你很在意你的命令耗时多少微秒的时候(当你尝试每秒处理十亿数据的时候,你就会在意),你就会很在意此类事情了。


觉得本文对你有帮助?请分享给更多人

关注「程序员的那些事」,看技术干货

↓↓↓

译者简介点击 → 加入专栏作者 )


艾凌风:初入职场小码农;翻译组的勤务员;C/Python/在线教育/英文翻译

打赏支持译者翻译出更多好文章,谢谢