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

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

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

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

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

3回答

2236閲覧

グローバルとmain()関数内で処理する場合のメモリ使用量の差

退会済みユーザー

退会済みユーザー

総合スコア0

Python 3.x

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

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

4クリップ

投稿2019/08/19 21:12

編集2019/08/20 06:18

DFS(深さ優先探索)のコードを書いていて、main()関数内で処理する場合、グローバル名前空間で処理するよりもメモリ使用量が増えていることに気づきました。
競技プログラミングのあるテストケースでメモリ制限が256MBのとき、main()のものだとMLE(>256MB)だったのが括弧を外すと215MBとなりました。
関数内でローカルに処理するほうが実行時間が早くなるのは知っていたのですが、メモリに関しては調べても良い文献が出てきません。
どなたか理由をご存知ありませんか?

提出 #7024392 - AtCoder Typical Contest 001 (MLE)

Python

1# ATC001A - DFS 2import sys 3input = sys.stdin.readline 4sys.setrecursionlimit(10 ** 6) 5 6def main(): 7 def dfs(h: int, w: int) -> None: 8 if (h, w) == goal: 9 print("Yes") 10 exit() 11 for dx, dy in dxy: 12 x, y = h + dx, w + dy 13 if 0 <= x < H and 0 <= y < W and F[x][y] != "#": 14 F[x][y] = "#" 15 dfs(x, y) 16 17 H, W = tuple(map(int, input().rstrip().split())) 18 F = list(list(input().rstrip()) for _ in range(H)) 19 dxy = [(1, 0), (0, 1), (-1, 0), (0, -1)] 20 flg = 0 21 for i in range(H): 22 for j in range(W): 23 if F[i][j] == "s": 24 start = i, j 25 flg += 1 26 elif F[i][j] == "g": 27 goal = i, j 28 flg += 1 29 if flg == 2: 30 break 31 dfs(*start) 32 print("No") 33 34 35if __name__ == "__main__": 36 main()

提出 #7024438 - AtCoder Typical Contest 001 (AC)

Python

1# ATC001A - DFS 2import sys 3input = sys.stdin.readline 4sys.setrecursionlimit(10 ** 6) 5 6def dfs(h: int, w: int) -> None: 7 if (h, w) == goal: 8 print("Yes") 9 exit() 10 for dx, dy in dxy: 11 x, y = h + dx, w + dy 12 if 0 <= x < H and 0 <= y < W and F[x][y] != "#": 13 F[x][y] = "#" 14 dfs(x, y) 15 16H, W = tuple(map(int, input().rstrip().split())) 17F = list(list(input().rstrip()) for _ in range(H)) 18dxy = [(1, 0), (0, 1), (-1, 0), (0, -1)] 19flg = 0 20for i in range(H): 21 for j in range(W): 22 if F[i][j] == "s": 23 start = i, j 24 flg += 1 25 elif F[i][j] == "g": 26 goal = i, j 27 flg += 1 28 if flg == 2: 29 break 30dfs(*start) 31print("No")

追記

提出結果のスクリーンショットですが、軽いものを除けばほとんどのケースでグローバル空間に直接書いたほうがメモリ使用量が少なくなっています。
MLEになったものを含め、偶然の結果ではないと思うのですが...
(左がmain(), 右がグローバルのものです)
イメージ説明
イメージ説明

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

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

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

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

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

guest

回答3

0

注意:ここに書いてあることはすべて推測・憶測・個人的な感覚に基づく情報です

まず、main()ありとmain()なしの以下の2つのコードでは、速度に2倍近くの差が出ます。また一般論ですが、速度を出したいときには高速な記憶域をじゃぶじゃぶ使う、記憶域を節約したいときにはCPUをぶんぶん振り回す、というセオリーがコンピュータプログラムにはあると思います。


環境:Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)]

python

1# main()あり:速い 2from time import perf_counter 3 4def main(): 5 a = 0 6 t = perf_counter() 7 for _ in range(10 ** 8): 8 a += 1 9 print("%.03f sec" % (perf_counter() - t)) 10 11main()

python

1# main()なし:遅い 2from time import perf_counter 3 4a = 0 5t = perf_counter() 6for _ in range(10 ** 8): 7 a += 1 8print("%.03f sec" % (perf_counter() - t))

そして記憶域という観点から考えた上記の2つのコードの違いは、main()の名前空間です。main()の名前空間分、記憶域を多く使うことによってmain()ありのコードは速度が出るようになったと考えることができます。

さて、これらの前提を踏まえた上で質問のmain()ありのdfs関数を見てみると、dfs関数はmain関数の内部関数、かつ再起しながら関数の外側の変数を参照するという、うまく言えないですが名前空間的なじゃぶじゃぶ感が否めません。dfs関数が再起していくなかで「たぶん」名前空間が膨れ上がっているんじゃなかろうかと思います。

(駄目だ、時間を掛けたのに上手く論理を展開できない。。。力不足です。)

投稿2019/08/20 12:17

YouheiSakurai

総合スコア6142

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

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

0

ベストアンサー

推測でしかありませんが、上の方は関数内関数になっているため、普通の関数が実行される時に必要な(ローカル変数用の)メモリの他に、もう1つ"外の関数"への(あるいは"外の関数"のローカル変数の辞書への?)参照が保持されるのが、地味に効いているのではないでしょうか?

傍証として、グローバル変数への参照を一切持たないトップレベル関数として実装するとMLEがでませんが、

Python

1# これは通る 2import sys 3 4sys.setrecursionlimit(10 ** 6) 5 6 7def dfs(h: int, w: int, goal, dxy, H, W, F) -> None: 8 if (h, w) == goal: 9 print("Yes") 10 sys.exit() 11 for dx, dy in dxy: 12 x, y = h + dx, w + dy 13 if 0 <= x < H and 0 <= y < W and F[x][y] != "#": 14 F[x][y] = "#" 15 dfs(x, y, goal, dxy, H, W, F) 16 17 18def main(): 19 H, W = tuple(map(int, input().rstrip().split())) 20 F = list(list(input().rstrip()) for _ in range(H)) 21 dxy = [(1, 0), (0, 1), (-1, 0), (0, -1)] 22 flg = 0 23 for i in range(H): 24 for j in range(W): 25 if F[i][j] == "s": 26 start = i, j 27 flg += 1 28 elif F[i][j] == "g": 29 goal = i, j 30 flg += 1 31 if flg == 2: 32 break 33 dfs(start[0], start[1], goal, dxy, H, W, F) 34 print("No") 35 36 37if __name__ == "__main__": 38 main()

これを関数内関数にするとそれだけで(つまり、"外の関数"の変数を一切参照していないのでクロージャではないただの関数内関数であるにも関わらず)MLEになります。

Python

1# これはMLE 2import sys 3 4sys.setrecursionlimit(10 ** 6) 5 6 7def main(): 8 def dfs(h: int, w: int, goal, dxy, H, W, F) -> None: 9 if (h, w) == goal: 10 print("Yes") 11 sys.exit() 12 for dx, dy in dxy: 13 x, y = h + dx, w + dy 14 if 0 <= x < H and 0 <= y < W and F[x][y] != "#": 15 F[x][y] = "#" 16 dfs(x, y, goal, dxy, H, W, F) 17 18 H, W = tuple(map(int, input().rstrip().split())) 19 F = list(list(input().rstrip()) for _ in range(H)) 20 dxy = [(1, 0), (0, 1), (-1, 0), (0, -1)] 21 flg = 0 22 for i in range(H): 23 for j in range(W): 24 if F[i][j] == "s": 25 start = i, j 26 flg += 1 27 elif F[i][j] == "g": 28 goal = i, j 29 flg += 1 30 if flg == 2: 31 break 32 dfs(start[0], start[1], goal, dxy, H, W, F) 33 print("No") 34 35 36if __name__ == "__main__": 37 main()

投稿2019/08/22 00:27

編集2019/08/22 00:28
quickquip

総合スコア11038

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

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

退会済みユーザー

退会済みユーザー

2019/08/22 02:36

ありがとうございます。 quiquiさんの予想が最も頷けるものだったので、ベストアンサーにしました。
guest

0

元々がギリギリすぎるというだけでは?

あまり細かく検証していませんが、GCが走るタイミング次第では下のコードでも215MBでは走らなくてエラーになるとか、そんなパターンな気がします。

投稿2019/08/20 01:18

編集2019/08/20 01:19
hayataka2049

総合スコア30933

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

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

退会済みユーザー

退会済みユーザー

2019/08/20 06:20

確かにメモリ制限が厳しいのは事実なのですが、追記の画像の通り、処理が重めのケースでは軒並みメモリ使用量に差が出ています。 今後のためにも、その理由が知りたいです。 よろしくお願いします
hayataka2049

2019/08/20 09:42

理屈の上では、そんな大きな差は生じないはずです。単純に私が見落としている可能性もありますが(わかった方は回答していただけると幸いです)。 処理系内部のメモリ管理の最適化の話とかになってしまうと、正直お手上げなので、わからないかもしれません。
hayataka2049

2019/08/20 09:44

あと、関係ないとは思うけど、exitはプログラム中では呼ぶべきではないとされます。 グローバルで書いた方はsys.exitする、mainで書いた方はreturnとかでいいでしょう。 exitする瞬間に送出された例外がスタックフレームを膨れ上がらせるみたいな妄想もしたけど、さすがにないか。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.49%

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

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

質問する

関連した質問