PDA

View Full Version : حرفه ای: گرفتن حافظه رم خیلی زیاد در حالی که نیازی نیست. چیکار کنم؟!



vahid-p
پنج شنبه 25 اردیبهشت 1393, 19:51 عصر
سلام
برنامه ای دارم که یک فایل نسبتا بزرگ رو میخونه و کلمات متمایزش رو داخل یک HashMap یا یک آرایه قرار میده ( فرض کنید آرایمون مثلا 1 میلیون خونه داشته باشه ). تعداد کلمات متفاوت این سند حدود 583,863 کلمه، و تعداد کل کلمات ( تکراری و غیر تکراری ) 145913232 .
چند نکته مثلا در آرایه ای که تعریف کردیم مثلا چنین چیزی هست :
int[][] vocab=new int[1000000][2];
کلمات میرن تو خونه ای که از باقیمونده گیری Hashcode اشون بر سایز جدول به دست میاد و کلمات تکراری فقط کانتر یعنی ستون دوم رو افزایش میده. در نتیجه ما اینجا هیچ چیزی بیشتر از خونه های vocab بالا نداریم ( که فرض کنیم هر سطرش هم 8 بایت باشه بیش از 8 مگابایت نیست ). حالا نمیدونم چرا وقتی که اجرا میکنم فضای حافظه مصرفی حدود 1.5 گیگابایت هست!
از HashMap هم استفاده میکنم مثلا با 100000 ظرفیت اولیه هم به همین صورت است!

ما هیچ اطلاعات دیگه ای ذخیره نمیکنیم و تنها در طول خوندن فایل آبجکت های String ساخته میشن، Tokenize میشن و ... که آبجکت هایی هستند که استفاده میشن و بیشتر بهشون نیازی نداریم!
اگر تو VM option : -Xmx200M بذاریم هم برنامه exception java heap space میده. دستور system.gc() هم هیچ کاری نمیکنه. چیکار میشه کرد؟

vahid-p
پنج شنبه 25 اردیبهشت 1393, 20:00 عصر
اینم کدهاشون : ( یکیشون درست بشه کافیه )
1- استفاده مستقیم از HashMap

public class HashDic {

HashMap<String, int[]> hashMap;
FileInputStream fileInput;
final int tableSize=1000000;
public HashDic(String fileInputPath) {
hashMap = new HashMap<>(tableSize);
try {
fileInput = new FileInputStream(fileInputPath);
} catch (FileNotFoundException ex) {
Logger.getLogger(TrieDic.class.getName()).log(Leve l.SEVERE, null, ex);
}

}
public void makeDic() {
int i = 0,word=0,doc=0;
String str;
StringTokenizer token;
Scanner cin = new Scanner(fileInput);

int[] v;
int UID = 0;
while (cin.hasNext()) {
token = new StringTokenizer(cin.nextLine());
while (token.hasMoreTokens()) {
str = token.nextToken();
if(str.equals("<p>")) doC++‎‎‎‎;
word++;
if ((v = hashMap.get(str)) == null) {
v = new int[2];
v[0] = UID;
v[1] = 1;
hashMap.put(str, v);
UID++;
} else {
v[1]++;
}
}
i++;
if (i >= 40) { // این شرط مهم نیست. فقط برای نشون دادن وضعیت پس از خواندن هر 40 خط است
i = 0;
Runtime runtime = Runtime.getRuntime();
long allocatedMemory = runtime.totalMemory();
System.out.println("allocated memory: " + (allocatedMemory / 1024)+ " word: "+word + " - New word : " + UID + " - Doc :"+doc);
}
}
try {
fileInput.close();
} catch (IOException ex) {
Logger.getLogger(TrieDic.class.getName()).log(Leve l.SEVERE, null, ex);
}
}

2- استفاده از hashcode برای دسترسی مستقیم به خانه های آرایه ( البته امکان کلمات متفاوت با خانه های مساوی هست که اونم میشه بعدا مرتفعش کرد )

public void makeDic() {
int i = 0, word = 0, doc = 0;
int hash, abshash;
String str;
StringTokenizer token;
Scanner cin = new Scanner(fileInput);
int[] v;
int UID = 0;

while (cin.hasNext()) {
token = new StringTokenizer(cin.nextLine());
while (token.hasMoreTokens()) {
str = token.nextToken();
word++;
hash = str.hashCode();
abshash = Math.abs(hash);
if (vocab[abshash % tableSize][0] != hash && vocab[abshash % tableSize][1] > 0) {
System.out.println("Duplicate (another hash): " + str);
} else {
if (vocab[abshash % tableSize][1] == 0) {
UID++;
}
vocab[abshash % tableSize][1]++;
vocab[abshash % tableSize][0] = hash;
}
}
i++;
if (i >= 40) { // این شرط مهم نیست. فقط برای نشون دادن وضعیت پس از خواندن هر 40 خط است
i = 0;
Runtime runtime = Runtime.getRuntime();
long allocatedMemory = runtime.totalMemory();
System.out.println("allocated memory: " + (allocatedMemory / 1024) + " word: " + word + " - New word : " + UID + " - Doc :" + doc);
}
}
try {
fileInput.close();
} catch (IOException ex) {
Logger.getLogger(TrieDic.class.getName()).log(Leve l.SEVERE, null, ex);
}
}

vahid-p
جمعه 26 اردیبهشت 1393, 19:28 عصر
یه نکته دیگه اضافه کنم، امروز تست گرفتم و پس از اتمام عملیات با همون رم مصرفی 1.5 گیگابایت، پس از 30 دقیقه، هش مپ حاصل ( فیلد HashMap<String, int[]> hashMap; ) رو توسط ObjectOutputStream سیو کردم و حجم فایل 16 مگ بیشتر نیست. حالا وقتی دوباره بازخونی میکنم رم اشغالی میشه 170 مگ. این نشون میده خود هش زیاد فضای رم رو اشغال نکرده و رم بیخودی اشغال شده. حتی پس از اون 30 دقیقه که کارش تموم شد و تو فایل Main.java اومدم کل اون آبجکت رو برابر null میذارم، باز هم رم آزاد نمیشه. ( گرچه بشه هم به دردم نمیخوره، چون من باید بتونم کل اینکارا رو تنها با 256 مگابایت رم انجام بدم! )

راستی اون فایلی که گفتم 1.5 گیگ بود، 1 گیگ بود. فایل اصلی 1.5 گیگ بود این فایل دومی است ولی باز رم 1.5 گیگ اشغال میشه.

cups_of_java
جمعه 26 اردیبهشت 1393, 20:04 عصر
موضوع HashMap نیست جون سایزش بزرگ نمیشه و اون سایز اینقدر مموری نمیگره. اما کار با Stringها ظرافت های زیادی داره که خیلی زود می تونه نشتی حافظه درست کنه. مخصوصن که Stringها اشیای Immutable هستن. مشکل باید در حین عملیات StringTokenizer باشه. (یا شاید احتمالن خوندن فایل ورودی توسط Scanner)

توصیه میکنم در اولین قدم به جای استفاده از StringTokenizer از تابع split و یا Regex ها استفاده کنی تا کلمه مورد نظر توی اون خط رو مچ کنی. بعدن hashcode اون رو توی آرایه به روز کنی.
نتیجه این کار رو خبر بده.

vahid-p
جمعه 26 اردیبهشت 1393, 20:41 عصر
از split استفاده کردم ولی تاثیری نداشت. نکته عجیبش اینه که اوایل مثلا تو 15 ثانیه اول رم اشغالی میرسه به 1.5 گیگ بعدش تا 30 دقیقه بعد که همون کارها رو داره انجام میده و کلمات جدید اضافه میشه باز همون مقدار میمونه.
دفعه قبلی که تست کردم نتایجش ( کلمات و حافظه تخصیصی و... رو ذخیره کردم که ضمیمه کردم ( فایل .txt ) اگه ممکنه یه نگاه بندازید چرا اینقدر عجیب غریبه!119118


اینم کد جدیدی که تست کردم.

public void makeDic() {
int i = 0, word = 0, doc = 0;
String wordInLine[];
Scanner cin = new Scanner(fileInput);

int[] v;
int UID = 0;
while (cin.hasNext()) {
wordInLine=cin.nextLine().split("\\s+");
for(i=0;i<wordInLine.length;i++){

if (wordInLine[i].equals("<p>")) {
doC++‎‎;
}
word++;
if ((v = hashMap.get(wordInLine[i])) == null) {
v = new int[2];
v[0] = UID;
v[1] = 1;
hashMap.put(wordInLine[i], v);
UID++;
} else {
v[1]++;
}
}
}
try {
fileInput.close();
} catch (IOException ex) {
Logger.getLogger(TrieDic.class.getName()).log(Leve l.SEVERE, null, ex);
}
}

cups_of_java
شنبه 27 اردیبهشت 1393, 10:33 صبح
خوب بدون اینکه از split و منطق شمارش و آرایت استفاده کنی. فقط با scanner خط به خط بخون ورودی رو تا تموم شه... ببین مموری چطوری بالا میره، ممکنه از خود Scanner باشه. اگه مموری باز بالا بود مدل خوندنت رو عوض کن.

vahid-p
شنبه 27 اردیبهشت 1393, 20:47 عصر
split رو ورداشتم و فایل رو مستقیم فقط خوندم ولی یه کم بهتر شد نه تا اون حد ( تو مراحل قبلی هم فایل رو خونده بودم از روش های دیگه و مشکلی نداشت ).
اومدم یه تست گرفتم و چک کردم ببینم هر خط چند کاراکتر است. بعضی از خطوط از قضا 10 یا 20 یا 40 میلیون کاراکتر بود و این علتش بود. چون یک آبجکت که برابر 80MB کاراکتر است بعد کلش رو بخوام split یا token کنم خودش کلی فضای دیگه میبره که مثل اینکه این فضا برگشت نمیخوره ( این خطوط طولانی پرش های ناگهانی مصرف رم رو هم همراش داشت ). اما اومدم Scanner.next() استفاده کردم یعنی کلمه به کلمه خوندم، علاوه بر اینکه splitکردن نمیخواست، رم مصرفی به شدت پایین اومد و سرعت خوندن به شدت بالا رفت. مشکلم کلا حل شد. الان پس از انجام کل عملیات ذخیره سازی هش و همه چیز همون 170 تا بیشتر نمیشه.

ولی خب چیزی که اینجا دیده میشه، آبجکت های استرینگی که ساخته میشن کم کم Garbage collector اونها رو جمع میکنه. وگرنه همین یک کلمه یک کلمه هم متونست رم خیلی زیادی رو اشغال کنه و فکر کنم immutable بودن یه معنی دیگه میده درسته؟ یعنی یک String رو نمیشه تغییر داد، اگر تغییر بدی یک آبجکت کپی از اون ساخته میشه و تغییرات اعمال میشن، درسته؟
جالبه با دستور -Xmx200M در VM option باعث میشه جاوا GC ایش رو سریعتر به کار بگیره و روند اضافه شدن ملایم تر میشه. خودش حدس میزنه :)

ممنون از راهنمایی و کمک هایی که کردید.

cups_of_java
شنبه 27 اردیبهشت 1393, 21:28 عصر
خب پس اصلن فایل ورودی مورد دار بوده خودش... خوبی Scanner اینه که به صورت Stream شده ورودی رو می خونه و مصرف حافظه پایین میاد (ورودی کامل توی رم نگهداری نمیشه) نکته دیگه اینه که StringTikenizer کلن یه کلاس depricate شده هستش و خیلی پیاده سازی بهینه ای نداره...
Immutable بودن اشیا به همین معنی هستش که قابل تغییر نیستن و برای هر تغییری یه کپی ساخته میشه و روی اون تغیر اعمال شده... کار کردن با Stringها تو مقیاس های بزرگ بسیاز tricky هستش ;)

vahid-p
شنبه 27 اردیبهشت 1393, 21:52 عصر
آره فایل مورد دار بود... خودمون هم مورد دارش کردیم و کلی بلا سرش در آوردیم و عمدا اینکار رو کرده بودیم که بدبختمون کرد

اینجا واقعا نجات یافتم.
مدتی بود هر چی میخواستم با جاوا انجام میدادم و کیف میکردم و اینجا بدجور گیر کردم و وقت هم کم بود. کم کم داشتم میرفتم با سی پلاس پلاس انجام بدم ( و میدونستم چه پدری ازم در میاد ) که نهایتا نجات یافتم و به آغوش گرم جاوا بازگشتم :لبخند:

L u k e
دوشنبه 05 خرداد 1393, 12:08 عصر
با نرم افزار Visual VM مانیتور می کردی می دیدی چه خبره.
من تو یه پروژه به یه مسئله برخوردم مطمئن نیستم.
تو جاوا یه حافظه رزرف شده داریم وقتی که برنامه شما برای مثال یه لحظه 1 گیگ رم اشغال می کنه و بعد GC می یاد حافظه رو خالی می کنه و مصرف حافظه می یاد پایین شما مصرف حافظه پروسس رو چک کنی می بینی که همون 1 گیگه ولی وقتی ماینتور کنی می بینی که خیلی کمتره.

vahid-p
دوشنبه 05 خرداد 1393, 13:36 عصر
با نرم افزار Visual VM مانیتور می کردی می دیدی چه خبره.
من تو یه پروژه به یه مسئله برخوردم مطمئن نیستم.
تو جاوا یه حافظه رزرف شده داریم وقتی که برنامه شما برای مثال یه لحظه 1 گیگ رم اشغال می کنه و بعد GC می یاد حافظه رو خالی می کنه و مصرف حافظه می یاد پایین شما مصرف حافظه پروسس رو چک کنی می بینی که همون 1 گیگه ولی وقتی ماینتور کنی می بینی که خیلی کمتره.
حق با شماست. Used Heap با Heap Size متفاوته ولی بلاخره من برای همون یه لحظه هم نمیخواستم Heap Size به 1 گیگ برسه. فرض کنید روی سیستمی باشه که کل رمش 1 گیگ باشه، برنامه متوقف میشه. GC هم وقتی با Visual VM چک میکنم، واقعا کارش خوبه و حافظه رو به موقع آزاد میکنه، ولی به هر حال Heap Size معمولا کم نمیشه.