LINUX.ORG.RU

Программа на Go потребляет очень много оперативной памяти.

 


0

2

Добрый день. Написал программу, которая тупо открывает csv файлы и читает их. Размер файлов около 100 МБ. Файлы полностью считываются, сортируются и закрываются. Проблема в том, что программа в какой-то момент заняла около 1200 МБ, после этого я остановил работу программы.

package main

import (
	"bufio"
	"encoding/csv"
	"fmt"
	"os"
	"path/filepath"
	"log"
	"sort"
)

type FInfo struct {
	path string
	FileInfo os.FileInfo
}

type FInfoSlice []FInfo

func (slice FInfoSlice) Len() int {
	return len(slice)
}

func (slice FInfoSlice) Less(i, j int) bool {
	return slice[j].FileInfo.ModTime().Before(slice[i].FileInfo.ModTime())
}
func (slice FInfoSlice) Swap(i, j int) {
	slice[i], slice[j] = slice[j], slice[i]
}

func getListFiles(paths []string) []string {
	count_paths := len(paths)
	info := make(FInfoSlice, count_paths)
	for index, path := range paths {
		info[index].FileInfo, _ = os.Stat(path)
		info[index].path = path
	}
	sort.Sort(info)
	new_paths := paths
	for index, f_struct := range info {
		new_paths[index] = f_struct.path
	}
	return new_paths
}

type csvSlice [][]string

func (c csvSlice) Len() int {
	return len(c)
}
func (c csvSlice) Swap(i, j int) {
	c[i], c[j] = c[j], c[i]
}
func (c csvSlice) Less(i, j int) bool {
	return c[i][4] < c[j][4]
}

func readCsv(path string) [][]string {
	file, err := os.Open(path)
	defer file.Close()
	if err != nil {
		log.Fatal(err)
	}
	reader := csv.NewReader(bufio.NewReader(file))
	reader.Comma = ';'
	values, _ := reader.ReadAll()
	sort_values := values[1:]
	sort.Sort(csvSlice(sort_values))
	return sort_values
}

func in(name string, info map[string]*csv.Writer) bool {
	for i_name, _ := range info {
		if i_name == name {
			return true
		}
	}
	return false
}

func writeCsvFile(path string) {
	for range readCsv(path) {
		continue
	}
}

func main() {
	pattern_out := fmt.Sprintf("%s/*.csv", os.Args[2])
	old_files, _ := filepath.Glob(pattern_out)
	for _, path := range old_files {
		os.Remove(path)
	}

	pattern_in := fmt.Sprintf("%s/*.csv", os.Args[1])
	paths, err := filepath.Glob(pattern_in)
	if err != nil {
		log.Fatal(err)
	}
	f_info := getListFiles(paths)
	for _, path := range f_info {
		writeCsvFile(path)
	}
}

Есть подозрение, что как-то не так работает уборка мусора или я что-то делаю не так.

go version go1.6 linux/386

★★

1. Там где критично использование памяти или производительность, не стоит использовать го. 2. Не стоит стоит работать с большими файлами загружая их целиком в оперативную память.

Esteban_Garcia
()

Пару мелких замечаний. Этот вот кусок не очень верный.

	file, err := os.Open(path)
	defer file.Close()
	if err != nil {
		log.Fatal(err)
	}
	reader := csv.NewReader(bufio.NewReader(file))
Надо бы:
	file, err := os.Open(path)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close() // закрытие имеет только смысл, если не было ошибки
	reader := csv.NewReader(file) // bufio лишний os.File удовлетворяет io.Reader

beastie ★★★★★
()
Ответ на: комментарий от Esteban_Garcia

На мой взгляд 100 МБ это не сильно большой файл, но отсортировать данные надо.

dicos ★★
() автор топика

Программа на Go потребляет очень много оперативной памяти.

«Это не баг, это фича».

ovk48 ★★★
()
Ответ на: комментарий от beastie

Спасибо за замечания, сейчас разбираюсь с pprof

dicos ★★
() автор топика
Ответ на: комментарий от ovk48

как бы предполагается автоматическая уборка мусора, тот же питон в аналогичной программе меньше памяти ест

dicos ★★
() автор топика

Попробовал сгенерить простой тест (1000 файлов по 2MB каждый):

#!/bin/bash

mkdir -p in/
mkdir -p out/

for i in `seq 1 100000`; do
    printf "1;2;3;4;5;6;7;8;9;10\n"
done > in/yow.csv

for i in `seq 1 1000`; do
    ln -s yow.csv in/"$i".csv
done

Не ест больше 200MB (amd64):

$ /usr/bin/time ./csv-parser in/ out/

237.46user 1.70system 2:45.44elapsed 144%CPU (0avgtext+0avgdata 204480maxresident)k

Сколько у вас файлов?

Сколько весит самый большой файл?

Как примерно выглядят строки?

sf ★★★
()
Ответ на: комментарий от Esteban_Garcia

1. Там где критично использование памяти или производительность, не стоит использовать го

желтовато

kelyar ★★★★★
()
Ответ на: комментарий от kelyar

Что не так. Есть хоть одна новость, чтобы что-то переписанное с си, плюсов или раста на го, было менее прожорливым и более производительным. Обратные случае вполне есть. А го заруливает в этом плане джаву, да.

Или взять ТС. Он что, не может сказать где у него лишний раз используется копирование? Это для того что он сам написал.

Esteban_Garcia
()

Какой же ужасный этот ваш Го. Свифт в отличие от него намного приятнее.

но отсортировать данные надо

Это всё только ради сортировки? Чувак, не страдай хернёй, пиши на питоне.

open('sorted.csv', 'w').writelines(sorted(open('unsorted.csv')))

100 мегабайт за 1 секунду. В sorted(), как ты знаешь, можно передать любую функцию для сортировки через аргумент key. Ещё в питоне есть готовый модуль csv.

anonymous
()
Ответ на: комментарий от Esteban_Garcia

'критично' это не значит что 'чем меньше тем лучше'. Это значит не больше чем надо при остальных плюсах и минусах

Debasher ★★★★★
()
Ответ на: комментарий от sf

Самый большой файл: https://yadi.sk/d/90UcvGvkqQkQW

Формат строк такой:

RIM0;RTS-6.10;159165.00000;1;2010-04-20 19:00:00.023;160084199;0
RIM0;RTS-6.10;159180.00000;1;2010-04-20 19:00:00.023;160084200;0
RIM0;RTS-6.10;159240.00000;1;2010-04-20 19:00:00.633;160084201;0
RIM0;RTS-6.10;159240.00000;1;2010-04-20 19:00:00.633;160084202;0
RIM0;RTS-6.10;159235.00000;1;2010-04-20 19:00:01.477;160084203;0
RIM0;RTS-6.10;159235.00000;1;2010-04-20 19:00:01.523;160084204;0
RIM0;RTS-6.10;159200.00000;1;2010-04-20 19:00:01.697;160084205;0
RIM0;RTS-6.10;159200.00000;2;2010-04-20 19:00:01.710;160084206;0
RIM0;RTS-6.10;159235.00000;1;2010-04-20 19:00:01.977;160084207;0

Файлов около 2000, средний размер файла 100 МБ.

dicos ★★
() автор топика
Ответ на: комментарий от anonymous

Код на питоне, который делает тоже самое есть, он памяти потребляет меньше. Это мне кажется очень странным.

Данный скрипт пишу для изучения. Сделать что-то полезное + научиться.

dicos ★★
() автор топика
Ответ на: комментарий от beastie

Ещё:

func in(name string, info map[string]*csv.Writer) bool {
	for i_name, _ := range info {
		if i_name == name {
			return true
		}
	}
	return false
}

Kiss:

func in(name string, info map[string]*csv.Writer) bool {
        _, ok := info[name]
        return ok	
}

beastie ★★★★★
()

Кажется, кто-то отведал goвна.

anonymous
()
Ответ на: комментарий от plotnikovanton

Да у тебя же все копируется. Ресиверы у методов тоже. Попробуй переписать методы так чтобы они были от указателей. Тут почитай еще

Лучше сначала ты сходи почитай про слайсы, да.

korvin_ ★★★★★
()
Ответ на: комментарий от dicos

Дамп кучи сразу перед сортировкой файла:

func readCsv(path string) [][]string {
        file, err := os.Open(path)
        defer file.Close()
        if err != nil {
                log.Fatal(err)
        }
        reader := csv.NewReader(bufio.NewReader(file))
        reader.Comma = ';'
        values, _ := reader.ReadAll()
        sort_values := values[1:]

        dump("before_sort.hprof") /// <- добавил
        sort.Sort(csvSlice(sort_values))
        dump("after_sort.hprof") /// <- добавил

        return sort_values
}
/// |
/// v добавил
func dump(path string) {
        f, err := os.Create(path)
        if err != nil {
                log.Fatal(err)
        }
        pprof.WriteHeapProfile(f)
        f.Close()
//...
}

Heap profile показывает, что объекты в куче занимают чуть больше размера самого файла (и до и после сортировки):

$ go build csv-parser.go && ./csv-parser in/ out/

$ go tool pprof csv-parser before_sort.hprof 
Entering interactive mode (type "help" for commands)
(pprof) top10
213.50MB of 213.50MB total (  100%)
      flat  flat%   sum%        cum   cum%
  187.01MB 87.60% 87.60%   187.01MB 87.60%  encoding/csv.(*Reader).parseRecord
   26.48MB 12.40%   100%   213.50MB   100%  encoding/csv.(*Reader).ReadAll
         0     0%   100%   187.01MB 87.60%  encoding/csv.(*Reader).Read
         0     0%   100%   213.50MB   100%  main.main
         0     0%   100%   213.50MB   100%  main.readCsv
         0     0%   100%   213.50MB   100%  main.writeCsvFile
         0     0%   100%   213.50MB   100%  runtime.goexit
         0     0%   100%   213.50MB   100%  runtime.main

Попробовал запустить на каталог из копий этого 180MB файла со статистикой GC (GODEBUG=gctrace=1):

$ GODEBUG=gctrace=1 ./csv-parser in/ out/

Вывод программы:

gc 1 @0.049s 1%: 0.15+1.6+0.23 ms clock, 0.47+0.93/3.0/2.8+0.70 ms cpu, 4->4->2 MB, 5 MB goal, 8 P
gc 2 @0.085s 1%: 0.010+1.7+0.21 ms clock, 0.082+0.92/3.2/5.1+1.7 ms cpu, 4->4->4 MB, 5 MB goal, 8 P
gc 3 @0.137s 2%: 0.009+19+0.10 ms clock, 0.079+3.0/19/9.0+0.86 ms cpu, 8->8->6 MB, 9 MB goal, 8 P
gc 4 @0.220s 2%: 0.012+4.3+0.067 ms clock, 0.10+0.55/8.0/17+0.54 ms cpu, 13->13->11 MB, 14 MB goal, 8 P
gc 5 @0.289s 2%: 0.011+6.6+0.22 ms clock, 0.088+0.99/12/28+1.8 ms cpu, 21->21->18 MB, 22 MB goal, 8 P
gc 6 @0.459s 2%: 0.011+10+0.21 ms clock, 0.092+1.8/21/48+1.7 ms cpu, 34->35->30 MB, 35 MB goal, 8 P
gc 7 @0.706s 2%: 0.013+55+0.057 ms clock, 0.11+7.8/76/0.92+0.45 ms cpu, 64->64->51 MB, 65 MB goal, 8 P
gc 8 @1.036s 3%: 0.024+132+0.077 ms clock, 0.19+20/134/56+0.61 ms cpu, 95->95->79 MB, 102 MB goal, 8 P
gc 9 @1.636s 3%: 0.026+46+0.23 ms clock, 0.21+1.3/88/215+1.8 ms cpu, 154->155->131 MB, 159 MB goal, 8 P
gc 10 @2.448s 2%: 0.034+75+0.22 ms clock, 0.27+3.5/149/360+1.8 ms cpu, 252->255->217 MB, 259 MB goal, 8 P
...
gc 26 @21.380s 1%: 0.009+42+0.24 ms clock, 0.078+2.7/82/207+1.9 ms cpu, 139->140->120 MB, 142 MB goal, 8 P
gc 27 @22.066s 1%: 0.017+137+0.33 ms clock, 0.14+40/190/94+2.6 ms cpu, 246->247->195 MB, 247 MB goal, 8 P
gc 28 @23.576s 1%: 0.010+118+0.20 ms clock, 0.085+5.1/236/575+1.6 ms cpu, 374->379->320 MB, 390 MB goal, 8 P
gc 29 @25.498s 1%: 0.010+185+0.082 ms clock, 0.083+4.1/362/845+0.66 ms cpu, 612->620->527 MB, 629 MB goal, 8 P
gc 30 @29.788s 1%: 0.065+306+0.25 ms clock, 0.52+45/440/32+2.0 ms cpu, 1048->1048->305 MB, 1049 MB goal, 8 P
gc 31 @31.874s 1%: 0.010+175+0.22 ms clock, 0.085+18/344/788+1.8 ms cpu, 594->601->507 MB, 610 MB goal, 8 P
gc 32 @36.010s 1%: 0.011+90+0.22 ms clock, 0.091+2.3/180/437+1.7 ms cpu, 978->981->271 MB, 1003 MB goal, 8 P
gc 33 @37.819s 1%: 0.015+151+0.24 ms clock, 0.12+11/299/737+1.9 ms cpu, 523->528->453 MB, 536 MB goal, 8 P
gc 34 @41.419s 1%: 0.011+55+0.21 ms clock, 0.089+4.0/109/255+1.7 ms cpu, 874->876->162 MB, 896 MB goal, 8 P

Видно, что размер кучи прыгает от 120MB до 1050MB.

Насколько я понимаю механизм выделения памяти в go (https://golang.org/pkg/runtime/ - описание переменной GOGC): сборка мусора не начнется до тех пор, пока объем свежевыделеннх данных не превысит данные, которые находятся в куче после предыдущей сборки мусора.

В нашем случае худший случай это сборка мусора прямо перед сортировкой массива: весь csv файл выживает сборку мусора и сборка мусора не начинается до загрузки следующего файла в память, что дает оверхед по памяти больше, чем в 2 раза: 213MB*2 ~500MB.

На сборку мусора тоже нужны какие-то ресурсы. В случае копирующего GC (не знаю, что использует go) свободной памяти в куче надо в худшем случае столько же, сколько ее уже занято. Итого ~500MB*2 = ~1GB :)

Пишут (https://blog.golang.org/go15gc), что в go concurrent-mark-and-sweep, но не пишут как решается проблема фрагментации кучи.

sf ★★★
()
Ответ на: комментарий от sf

Получается, руками память вообще не освободить? Я использовал вот эту функцию https://golang.org/pkg/runtime/debug/#SetGCPercent Вот так:

func readCsv(path string) [][]string {
	file, err := os.Open(path)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()
	reader := csv.NewReader(file)
	reader.Comma = ';'
	values, _ := reader.ReadAll()
	sort_values := values[1:]
	sort.Sort(csvSlice(sort_values))
	debug.SetGCPercent(0)
	debug.SetGCPercent(100)
	return sort_values
}
Но эффект не принесла эта функция.

dicos ★★
() автор топика
Ответ на: комментарий от dicos

Можно попробовать вызывать runtime.GC() сразу после обработки файла: https://golang.org/src/runtime/mgc.go?s=33002:33011#L829

        for _, path := range f_info {
                writeCsvFile(path)
                runtime.GC() /// <- здесь
        }

После этого использование heap у меня больше 700MB не поднимается.

sf ★★★
()

В чем проблема-то? Ну ест он память и что? У тебя все ушло в swap и не работает?

anonymous
()
Ответ на: комментарий от anonymous

Я писал на питоне (интерпретируемый язык) аналогичную программу, она памяти потребляет столько же, сколько программа, написанная на компилируемом языке. Я считаю это не правильно или я что-то делаю не то.

dicos ★★
() автор топика
Ответ на: комментарий от dicos

Как связаны компиляция и потребление памяти? Вот если бы вы написали программу на языке со сборщиком мусора и без, а потребление было бы одинаковое, тогда да, стоило удивляться.

Weres ★★★
()
Ответ на: комментарий от Weres

Всегда думал, что у интерпретируемых ЯП потребление памяти выше из-за загрузки интерпретатора в оперативную память, расходы на динамическую типизацию. У компилируемых ЯП таких расходов нет.

Но посмотрев тесты понял, что потребление оперативной памяти есть величина совершенно непредсказуемая.

dicos ★★
() автор топика
Ответ на: комментарий от Esteban_Garcia

Не стоит стоит работать с большими файлами загружая их целиком в оперативную память.

Именно так с ними и стоит работать.

registrant27492
()
Ответ на: комментарий от dicos

Всегда думал, что у интерпретируемых ЯП потребление памяти выше из-за загрузки интерпретатора в оперативную память, расходы на динамическую типизацию.

Это действительно так. Только го - это псевдокомпилируемый язык с гц, ибо там нечего компилировать - в твоей портянке ровно 0 кода и 100% вызова рантайма. Конпелятор уровня 70-х годов. А рантайм даже у «интерпретируемых» языков может быть компилируемым, хотя какие нынче «интерпретируемые», когда везде жит.

Но посмотрев тесты понял, что потребление оперативной памяти есть величина совершенно непредсказуемая.

Она предсказуемая. Во всех этих языках рантайм дерьмо, который сваяли кое как индусы на коленке.

Попытался я как-то там сделать -S, но конпелятор слишком никакой. Ну а в 3-х метрах бинаря мне копаться лень.

registrant27492
()
Ответ на: комментарий от sf

Не работает, ошибку выдает:

fatal error: runtime: cannot allocate memory

runtime stack:
runtime.throw(0x8141ee0, 0x1f)
	/home/dmitry/go/src/runtime/panic.go:530 +0x7f
runtime.persistentalloc1(0x800, 0x40, 0x81c0718, 0xbffe2800)
	/home/dmitry/go/src/runtime/malloc.go:937 +0x256
runtime.persistentalloc.func1()
	/home/dmitry/go/src/runtime/malloc.go:890 +0x35
runtime.systemstack(0x18435ec8)
	/home/dmitry/go/src/runtime/asm_386.s:329 +0x88
runtime.persistentalloc(0x800, 0x40, 0x81c0718, 0x66)
	/home/dmitry/go/src/runtime/malloc.go:891 +0x4e
runtime.getempty(0x65, 0xb761a000)
	/home/dmitry/go/src/runtime/mgcwork.go:347 +0x87
runtime.(*gcWork).init(0x1841c928)
	/home/dmitry/go/src/runtime/mgcwork.go:97 +0x21
runtime.(*gcWork).put(0x1841c928, 0x2a281b40)
	/home/dmitry/go/src/runtime/mgcwork.go:113 +0x34
runtime.greyobject(0x2a281b40, 0x0, 0x0, 0x17217e4b, 0x0, 0x7c4865a8, 0x1841c928)
	/home/dmitry/go/src/runtime/mgcmark.go:1090 +0x295
runtime.shade(0x2a281b40)
	/home/dmitry/go/src/runtime/mgcmark.go:1028 +0x9b
runtime.gcmarkwb_m(0x19cc68a4, 0x2a281b40)
	/home/dmitry/go/src/runtime/mbarrier.go:94 +0xa8
runtime.writebarrierptr_nostore1.func1()
	/home/dmitry/go/src/runtime/mbarrier.go:120 +0x110
runtime.systemstack(0x1841c000)
	/home/dmitry/go/src/runtime/asm_386.s:313 +0x5e
runtime.mstart()
	/home/dmitry/go/src/runtime/proc.go:1048

goroutine 1 [running]:
runtime.systemstack_switch()
	/home/dmitry/go/src/runtime/asm_386.s:267 fp=0x1852ab70 sp=0x1852ab6c
runtime.writebarrierptr_nostore1(0x19cc68a4, 0x2a281b40)
	/home/dmitry/go/src/runtime/mbarrier.go:121 +0x5c fp=0x1852ab8c sp=0x1852ab70
runtime.writebarrierptr_nostore(0x19cc68a4, 0x2a281b40)
	/home/dmitry/go/src/runtime/mbarrier.go:159 +0x5a fp=0x1852ab98 sp=0x1852ab8c
runtime.heapBitsBulkBarrier(0x19cc68a4, 0xc)
	/home/dmitry/go/src/runtime/mbitmap.go:437 +0x1ac fp=0x1852abe4 sp=0x1852ab98
runtime.typedmemmove(0x80f9d40, 0x19cc68a4, 0x2b1108a4)
	/home/dmitry/go/src/runtime/mbarrier.go:197 +0x82 fp=0x1852abfc sp=0x1852abe4
runtime.growslice(0x80f9300, 0x2a39a000, 0x169800, 0x169800, 0x169801, 0x0, 0x0, 0x0)
	/home/dmitry/go/src/runtime/slice.go:105 +0x2b9 fp=0x1852ac3c sp=0x1852abfc
encoding/csv.(*Reader).ReadAll(0x18418180, 0x2a39a000, 0x169800, 0x169800, 0x0, 0x0)
	/home/dmitry/go/src/encoding/csv/reader.go:170 +0x145 fp=0x1852ac74 sp=0x1852ac3c
main.readCsv(0x1854d410, 0x30, 0x0, 0x0, 0x0)
	/home/dmitry/web/moex_info_/moex_info/catalog.go:66 +0x1fa fp=0x1852ad5c sp=0x1852ac74
main.writeCsvFile(0x1854d410, 0x30)
	/home/dmitry/web/moex_info_/moex_info/catalog.go:79 +0xe9 fp=0x1852aef8 sp=0x1852ad5c
main.main()
	/home/dmitry/web/moex_info_/moex_info/catalog.go:115 +0x3c5 fp=0x1852afb0 sp=0x1852aef8
runtime.main()
	/home/dmitry/go/src/runtime/proc.go:188 +0x276 fp=0x1852afd8 sp=0x1852afb0
runtime.goexit()
	/home/dmitry/go/src/runtime/asm_386.s:1585 +0x1 fp=0x1852afdc sp=0x1852afd8

real	142m49.837s
user	122m18.900s
sys	4m24.064s
Оперативной памяти на компьютере 4 ГБ.

dicos ★★
() автор топика
Ответ на: комментарий от dicos

Похоже на https://github.com/golang/go/issues/12233

TL;DR: баг ядра в подсистеме transparent huge pages (go аллокатор их использует), страницы не мержатся в одну линейную область (VMA).

Для проверки предлагают увеличить значение

   vm.max_map_count = 65530
до чего-нибудь большего:
   sysctl vm.max_map_count=131072
При наличии ошибки ядра количество областей должно со временем расти.
    pmap -p $(pidof csv-parser) | wc -l
У меня не растет и остается равным 18.

Если роста VMA не наблюдается - значит что-то еще утекает.

GODEBUG=gctrace=1

должен показать растет ли размер кучи.

https://github.com/golang/go/blob/master/src/runtime/mem_linux.go#L75 : текущая реализация (с хорошим комментарием) до сих пор использует huge pages с какими-то эвтистиками для подавления роста VMA.

sf ★★★
()
Ответ на: комментарий от dicos

Всегда думал, что у интерпретируемых ЯП потребление памяти выше из-за загрузки интерпретатора в оперативную память, расходы на динамическую типизацию

Эти расходы могут быть десятыми долями процента от общего объема памяти, которую занимает программа.

tailgunner ★★★★★
()
Ответ на: комментарий от dicos

Код на питоне, который делает тоже самое есть, он памяти потребляет меньше. Это мне кажется очень странным.

Значит у этого поделия сборщик мусора даже хуже пытоньего. Ничего удивительного, язык пишут древние деды-самоучки. Для них и такой рантайм - подвиг.

anonymous
()
Ответ на: комментарий от tailgunner

Эти расходы могут быть десятыми долями процента от общего объема памяти, которую занимает программа.

Если под «программой» понимается «выделить буфер и скинуть его в бинарный рантайм без гц» - то да, во всех остальных случаях нет.

registrant27492
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.