質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Q&A

解決済

3回答

938閲覧

Pythonの計算コストについて

ugokumaitake

総合スコア11

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

0グッド

5クリップ

投稿2018/05/03 13:56

困っていること

AtcoderのABC081のC問題を解いていたところ、私がはじめに書いたコードはTLE(実行制限時間超過)となってしまいました。
その後、ソースコードを修正し、そのコードは通ったのですが、最初に書いたコードのどの処理にコストが高くかかってしまったのかいまいちわからないため、教えていただきたいです。

問題とそのテストケースのリンクは以下の通りです。
ABC081 C問題
テストケース(DropBox)

pythonのバージョンは3.6.5を使用しています。

TLEとなったソースコード

from collections import Counter n, k = map(int, input().split()) a = Counter((i for i in input().split())) sort_count = sorted(a.items(), key=lambda x: x[1]) total = 0 while len(sort_count) > k: _, n = sort_count.pop(0) total += n print(total)

修正後のソースコード

from collections import Counter n, k = map(int, input().split()) a = Counter((i for i in input().split())) sort_count = sorted(a.values()) total = 0 l = len(sort_count) if len(sort_count) <= k: print(0) else: print(sum(sort_count[:l-k]))

試したこと

修正前のコードでは、出現回数を昇順にソートしたものから1つずつpopしてtotalを算出しています。

修正後のコードでは、出現回数のみを格納したソートされたリストからpythonの組込関数であるsumを用いてtotalを算出しています。

勉強不足で申し訳ございませんが、どの部分の処理にコストが高くかかっているのか知りたいです。
よろしくお願いします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

面白そうなので、line_profilerというものを使ってプロファイリングしてみました。

参考:
Python: line_profiler でボトルネックを調べる - CUBE SUGAR CONTAINER

道具立てのためにbefore.pyとafter.pyを作り、それぞれline_profilerでプロファイリングします。

before.py

python

1from collections import Counter 2 3with open("1.txt", "r") as f: 4 text_str = f.read() 5 6n_k, data = text_str.split("\n")[:2] 7 8@profile 9def process(): 10 for x in range(100): 11 n, k = map(int, n_k.split()) 12 a = Counter((i for i in data.split())) 13 14 sort_count = sorted(a.items(), key=lambda x: x[1]) 15 16 total = 0 17 while len(sort_count) > k: 18 _, n = sort_count.pop(0) 19 total += n 20 21 #print(total) 22 total 23 24if __name__ == "__main__": 25 process()

after.py

python

1from collections import Counter 2 3with open("1.txt", "r") as f: 4 text_str = f.read() 5 6n_k, data = text_str.split("\n")[:2] 7 8@profile 9def process(): 10 for x in range(100): 11 n, k = map(int, n_k.split()) 12 a = Counter((i for i in data.split())) 13 14 sort_count = sorted(a.values()) 15 16 total = 0 17 l = len(sort_count) 18 19 if len(sort_count) <= k: 20 # print(0) 21 0 22 else: 23 # print(sum(sort_count[:l-k])) 24 sum(sort_count[:l-k]) 25 26if __name__ == "__main__": 27 process()

実行結果

text

1$ kernprof -lv before.py 2Wrote profile results to before.py.lprof 3Timer unit: 1e-06 s 4 5Total time: 0.017229 s 6File: before.py 7Function: process at line 8 8 9Line # Hits Time Per Hit % Time Line Contents 10============================================================== 11 8 @profile 12 9 def process(): 13 10 101 141.0 1.4 0.8 for x in range(100): 14 11 100 504.0 5.0 2.9 n, k = map(int, n_k.split()) 15 12 100 8778.0 87.8 50.9 a = Counter((i for i in data.split())) 16 13 17 14 100 2164.0 21.6 12.6 sort_count = sorted(a.items(), key=lambda x: x[1]) 18 15 19 16 100 163.0 1.6 0.9 total = 0 20 17 1100 1858.0 1.7 10.8 while len(sort_count) > k: 21 18 1000 2092.0 2.1 12.1 _, n = sort_count.pop(0) 22 19 1000 1397.0 1.4 8.1 total += n 23 20 24 21 #print(total) 25 22 100 132.0 1.3 0.8 total 26 27$ kernprof -lv after.py 28Wrote profile results to after.py.lprof 29Timer unit: 1e-06 s 30 31Total time: 0.006542 s 32File: after.py 33Function: process at line 8 34 35Line # Hits Time Per Hit % Time Line Contents 36============================================================== 37 8 @profile 38 9 def process(): 39 10 101 71.0 0.7 1.1 for x in range(100): 40 11 100 258.0 2.6 3.9 n, k = map(int, n_k.split()) 41 12 100 5397.0 54.0 82.5 a = Counter((i for i in data.split())) 42 13 43 14 100 354.0 3.5 5.4 sort_count = sorted(a.values()) 44 15 45 16 100 70.0 0.7 1.1 total = 0 46 17 100 70.0 0.7 1.1 l = len(sort_count) 47 18 48 19 100 80.0 0.8 1.2 if len(sort_count) <= k: 49 20 # print(0) 50 21 0 51 22 else: 52 23 # print(sum(sort_count[:l-k])) 53 24 100 242.0 2.4 3.7 sum(sort_count[:l-k]) 54

とりあえずわかることはa = Counter((i for i in data.split()))の行が壮絶に遅いことですが、これはどちらにもあるので気にしないこととして、あとはbeforeの方はぜんぶ遅いですね。

遅い理由の説明

  • sort_count = sorted(a.items(), key=lambda x: x[1])

これをやるとaの中身をぜんぶ手繰り寄せた上で、キーでソートする(無駄にオブジェクトをたくさん作ったり、ポインタ見る回数が増えたり色々する)ので遅い。sorted(a.values())は値をぜんぶ取ってきてソートするだけなのでだいぶマシ。

以下はループ内のお話なので、とてもシビアに効いてきます。個々の計算はそうでもなくても、ループ回数がそれなりにありますから。

  • while len(sort_count) > k:

毎回len計算して比較するから。

  • _, n = sort_count.pop(0)

popという操作はオブジェクトを変更します。当然それなりに遅いです。

  • total += n

ただの加算なのに意外と時間取ってますね。ループの中だとこうなります(というか逆に、これと比較するとlen, 比較, popは優秀なのか・・・)。

投稿2018/05/03 14:32

編集2018/05/03 21:36
hayataka2049

総合スコア30933

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ugokumaitake

2018/05/04 14:18

丁寧な解説ありがとうございました。 ループ内の各処理とプロファイル結果から、どの操作に時間がかかっているのかが非常にわかりやすかったです。 ふと冷静に自分が書いたプログラムを見るとなんでこんな書き方したんだろうと恥ずかしくなりますね。 急いでプログラムを作っているとはいえ、各処理の正当性を確かめながらプログラムを書かなければいけないと再認識しました。 ありがとうございました。
guest

0

ベストアンサー

cProfile & snakeviz & timeitを使ったプロファイルの仕方です。

<< チューニングの原則 >>
1,プロファイラを使う。代表的なプロファイラとしてcProfileがあります。
2,チューニングのデッドラインを決める。
今回の場合は、質問文のリンク先の問題にある、実行時間制限: 2 sec が該当します。
チューニングすることによってこれ以下の実行時間にしないといけません。

◇手順
1,before.pyを作成しその後コピーしafter.pyを作成します。

Python

1from collections import Counter 2n, k = map(int, input().split()) 3a = Counter((i for i in input().split())) 4 5sort_count = sorted(a.items(), key=lambda x: x[1]) 6 7total = 0 8while len(sort_count) > k: 9 _, n = sort_count.pop(0) 10 total += n 11 12print(total)

変更するのはafter.pyです。

2,初回のプロファイルを取ります。入力ファイルはリダイレクトで渡します。
python -m cProfile -o before.prof before.py < 13.txt
python -m cProfile -o after.prof after.py < 13.txt

3,snakevizで、プロファイル結果を表示します。
snakeviz before.prof
snakeviz after.prof

4,プロファイル結果を元にボトルネックを特定します。
イメージ説明
実行時間が0.0998 sかかり、 そのうち0.0714 sがcollectioncountが占めているのが分かります。
直前の呼び出し部分がupdateなので、ソースコードのCounterをチェックします。

Python

1 a = Counter((i for i in data.split()))

Counterの引数がジェネレータ式(※1)になっているため、試しにinput().split()に変更してみます。
※1 hayataka2049さん指摘ありがとうございました。

※注意
ここから変更するソースコードはafter.pyです。

Python

1 a = Counter(input().split())

6,after.pyのプロファイルを再度取ります。
python -m cProfile -o after.prof after.py < 13.txt
snakeviz after.prof
イメージ説明
◇結果
0.0714 sから0.0598 sに高速化されました。
このような形で一番CPU時間を専有している部分から、最初に求めたデッドラインの時間を満たすまでチューニングをします。

◇参考情報
SNAKEVIZ


pythonのバージョンは3.6.5を使用しています。

すべての提出一覧を見る限りではAtCoderの実行環境はPython3 (3.4.3)のなためvenvなどで、仮想環境を構築して同じ環境(3.4.3)にしたほうが良いと思われます。

投稿2018/05/03 21:16

編集2018/05/03 23:18
umyu

総合スコア5846

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

hayataka2049

2018/05/03 21:26

a = Counter((i for i in data.split())) はtupleじゃなくてジェネレータ式です。 にしても、リスト内包の方が早くなるのか・・・
umyu

2018/05/03 21:28

>hayataka2049さんへ すみませぬううう。いえそもそも論としてこの形でデータを保持する必要があるのかという問題も。
hayataka2049

2018/05/03 21:32

この場合、 a = Counter(data.split()) でよさそうですが、これでもまだ激遅なので(オリジナルの半分程度の処理時間にしかならない)、Counter以外の手段で速くできるか・・・という挑戦のしがいがありそうですね(標準でも遅いモジュールはおっそいし、今回の目的に適うかもあるから)。
umyu

2018/05/03 23:04

>hayataka2049さんへ timeitモジュールを使ってループを100回するのをやめました。 あと標準のリダイレクトよりファイル入力を受け取る形に変更しました。
hayataka2049

2018/05/03 23:36

便利そうです。行で見えるのがなんとなく気に入ってline_profiler好んでましたけど、cProfileは標準だしsnakevizも良さそうです。cProfileは勝手にtimeitで測ってくれる感じですか?
umyu

2018/05/04 00:01

>hayataka2049さんへ 1,cProfileは勝手にtimeitで測ってくれませんー。。 2,snakevizは初期表示が円グラフなので、site-packages\snakeviz\templates\viz.htmlを弄って、棒グラフに変更してます。
hayataka2049

2018/05/04 00:03

あれ? ではtimeitで測ってるのは回答の中でどの部分ですか?>1
mkgrei

2018/05/04 03:32

素朴な疑問ですが、cProfileやline_profilerなどをいれてしまうと、for文を使う場合、回した分だけプロファイラのオーバーヘッドがかかってしまって計算時間に影響が出ませんか? 手元で実測すれば良いのですが、典型的な回避テクニックがあれば教えていただきたく…
umyu

2018/05/04 07:19

>mkgreiさんへ for文のオーバーヘッドに関しては気にしてませんでした。回避テクニックあるのでしょうか・・。 あと回答文に書いていない点で、プロファイラを取る時に気にしている点は、GC(Garbage Collection)の影響と1回ではなく複数回プロファイルを取るぐらいでしょうか。
ugokumaitake

2018/05/04 14:09

ご回答ありがとうございます。 早速教えていただいた手法でプロファイルをして見たところ、どの処理にコストがかかっているか一目瞭然で分かり易かったです。 恥ずかしながらプロファイルというものを知らなかったので、このような手法があることを知り目から鱗でした。 また、ご指摘いただいた通り、早速pythonのバージョンを3.4.3に変更いたしました ありがとうございます。
guest

0

2秒で20万のデータをですか。。
こんなのどうでしょう
空の配列用意してデータを読み込みながら重複を省く。
(javascript風の動かないやつですが単純ですのでご勘弁を)

javascript

1let a = [] 2for(i of input().split() ){ 3 a[parseInt(i)] = i 4} 5 6return max ( 0, a.length - k) 7

投稿2018/05/03 21:15

編集2018/05/03 21:17
vapordog

総合スコア192

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問