前提・実現したいこと
初心者です。
Excel出力を行う際のメモリ使用量が大きいので調べてほしいと頼まれ、
どこで発生しているのかを調査したところ、
GetCellData関数の
if(formula) data = range.GetFormula();
else data = range.GetValue(vtMissing);
で、メモリの使用量が大きく上がるのが分かりました。
しかし、どこに問題があるのかがまだ分からないので、教えてもらえないでしょうか。
該当のソースコード
void CExcelCtrl::SetCellData(short col, short row,LPCSTR str) { try{ Range range = m_excel.GetCells(); range.SetItem(COleVariant(row),COleVariant(col),COleVariant(str)); }catch(COleDispatchException *e){ AfxMessageBox(e->m_strDescription,MB_ICONEXCLAMATION); } } void CExcelCtrl::GetCellData(int cols,int rows,int cole,int rowe,CStringArray &dt,bool formula) { CString sc,ec; sc.Format("%s%d",Num2Col(cols),rows); ec.Format("%s%d",Num2Col(cole),rowe); GetCellData(sc,ec,dt,formula); } void CExcelCtrl::GetCellData(LPCSTR sc,LPCSTR ec,CStringArray &dt,bool formula) { try{ _Worksheet ws = m_excel.GetActiveSheet(); Range range = ws.GetRange(COleVariant(sc),COleVariant(ec)); COleVariant data; if(formula) data = range.GetFormula(); else data = range.GetValue(vtMissing); COleSafeArray sa; sa.Attach(data); long rowmax,colmax; sa.GetUBound(1,&rowmax); sa.GetUBound(2,&colmax); CString str; long idx[2]; dt.RemoveAll(); for(long row = 1;row <= rowmax;row++){ idx[0] = row; for(long col = 1;col <= colmax;col++){ idx[1] = col; COleVariant val; sa.GetElement(idx,&val); switch(val.vt){ case VT_R8: str.Format("%1.2f", val.dblVal); break; case VT_BSTR: str.Format("%s",(CString)val.bstrVal); break; case VT_EMPTY: str.Empty(); break; } dt.Add(str); } } }catch(COleDispatchException *e){ AfxMessageBox(e->m_strDescription,MB_ICONEXCLAMATION); dt.RemoveAll(); } }
RangeのGetCells関数
LPDISPATCH Range::GetCells() { LPDISPATCH result; InvokeHelper(0xee, DISPATCH_PROPERTYGET, VT_DISPATCH, (void*)&result, NULL); return result; }
試したこと
PageComvert関数のみ除外して、Excel出力を実行
PageComvert関数内のGetCellDataのみ除外して、Excel出力を実行
補足情報(FW/ツールのバージョンなど)
Windows10/64bit
VisualStudio 2017
なにをどうしたときに、何がどうなるのかを詳しく説明しましょう
あまりにも説明不足です
申し訳ありません。
説明を追加致しました。
うーん、ご提示の箇所が問題とは思えないんですよね・・・。ChangeDataで呼んでいる関数群とかそっちなんだろうか・・・
PageComvert関数だけ除外してみると、メモリの使用率がガクッと下がり、表紙の名前や日付、各ページの番号だけが未出力になるだけで、中身の件名や数値には問題ありませんでした。
出力後のメモリが200,000k→20,000kぐらいに違うので、どこかが問題だとはわかるのですが・・・
ChangeDataで呼んでいる関数も追加したほうがいいでしょうか?
COMオブジェクトの詳細部分のソース(怪しいのはRangeクラス?のソース)が無いので想像ですが、デストラクタでリリースするためのメソッドなどがちゃんと呼ばれていないのではないでしょうか。Rangeクラスのソースを可能であれば提示してもらえると解決につながるかもしれません。
ご回答ありがとうございます。
詳細部分のソースというのは、SetやGet,ChangeData関数で呼ばれている関数のことでしょうか。
ソースを見た限りではCExcelCtrl::SetCellData()で呼ばれている
Range range = m_excel.GetCells();
が怪しく見えますので、Rangeクラスがわかれば問題点が見えるのではないかと思っています。
GetCells()関数やCExcelCtrl::GetCellData()で呼ばれているGetCellData()関数も開示可能であれば、お願いします。
ご回答ありがとうございます。
関連するソースコードを追加致しました。
普通のMFCのラッパークラスっぽいですね。
PageConvert関数のループ処理内部で呼ばれているものをちょっとずつコメントして、どの処理がメモリを食っているか調べてみてはどうでしょうか。
ご回答ありがとうございます。
コメントアウトでの区切り方としては、どのように区切ればよろしいでしょうか。
どうやらGetCellData()関数のif(formula) data = range.GetFormula();
else data = range.GetValue(vtMissing);の時点でメモリ使用率が増加しています。
あまりメモリが増加しないようにするためにはどう修正を行えばいいのでしょうか。
Excel を COM で接続して操作するプログラムは何度か作成しました。#import を使ってタイプライブラリーを読み込む方法で作りましたが、MFC を使ったものも作った事があります。
私の経験からは、注意深く作ればメモリーリークはなかったと思っています。以前手がけたプロジェクトでは、プログラムが 24 時間ずっと稼働するもので、メモリーリークがあると困るので、慎重にテストした覚えがあります。そのシステムは現在も稼働しているはずです。
制作段階でいろいろなサンプルを作って確認しましたが、エクセルからデータを取り込む際に、データ量によってはかなりのメモリーを使用しました。特にある程度大きなセル範囲を一度に取り込むと、一時的にですが、300MB を超える事もありました。
メモリー使用量を抑えるためには、セルの範囲をできるだけ小さくすることが効果的だと思います。大きな範囲を取り込む必要があるなら複数回に分けるのがよいのではないかと思います。
ご回答ありがとうございます。
こちらで起きている症状も恐らく同じようなものであると思います。
セルの範囲を小さくする、または、大きな範囲を取り込む際には複数回分けるには、どういったやり方があるのでしょうか。
今さらながらソースを拝見しました。見間違いでなければ、一度に255列を一行ずつ取り込んでいるようです。私がテストしたときは20列×500行(セルの内容は2000文字ほど文字列で)を一度に取り込んでいましたので、状況が違うかも知れません。
とはいえ、分割して取り込むのは有効かもしれないので、一度に20列ほどにしてはいかがでしょうか? ソースを読む限りCStringArray にセルの内容を文字列にして追加しているようなので、分割はたやすいように思えます。
前のコメントでも書きましたが、私のプログラムは #import を使用しています。ご質問のプログラムを読む限り、そうではないようですので前提が違うのかもしれません。
ご回答ありがとうございます。
取り込む範囲を変更して、様子を見てみます。
一旦、取り込む範囲を50行に変更したらメモリの使用量も70Mに小さくなりました。
20行にしたところ、50Mに減っていました。
出力結果にも問題はありません。
しかし、問題となっていた箇所を
COleSafeArray sa;
if(fomula) sa.Attach(range.GetFormula());
else sa.Attach(range.GetValue(vtMissing));
のように変更して出力したところ、メモリ使用量が25Mほどになっていました。
つまりdataへの代入が悪かったということなのでしょうか。
それでもCOleVariantなら解放されるはずなのですが・・・
こちらでも同様な現象を確認しました。どうもメモリーリークしているようです。こちらの環境でテストしたプログラムは、MFCのウィザードが作成したヘッダーを使用しています。
こちらの環境ではメモリーリークは COleVariant ではなく get_Value(質問のソースでは GetValue)で起きているようでした。
get_Value と GetValue が同じものかどうかはわかりませんが、get_Value は VARIANT を返しているのですが、それを COleVariant で受けるとメモリーリークしてしまうようです。
とりあえず、
VARIANT data;
VariantInit(&data);
data = range.get_Value(covOptional);
として、最後に
VariantClear(&data);
とするとメモリーリークは収まりました。
ご回答ありがとうございます。
そちらでも同じようなことが・・・
修正前では、GetFomulaの方でメモリが増加し、GetVauleの方では使用量がGetFomulaの半分でした。
直接入れてもメモリリークは起こりませんし、VARIANTで宣言しても起こらないとなると、COleVariantがメモリ増加の原因ということでしょうか。
それともrangeの代入処理が悪いのでしょうか。
COleVariant::Attach()を使用すると、COleVariantでも大丈夫なようです。
if(formula) data.Attach(range.GetFormula());
else data.Attach(range.GetValue(vtMissing));
先のコメントでは、GetValue がメモリーリークを起こしていような事を書きましたが、正確ではなかったです。
もし、GetValue と GetFomula が VARIANT を返しているとするとどうなるか、少し説明します。
COleVariant はクラスですのでコンストラクターや代入演算子が定義されています。それを見てみると、VariantCopy を呼んでいます。
VARIANT が VT_BSTR の場合は文字列がコピーされます。しかし、VARIANT は単純な構造体なので、デストラクターは定義されていません。コピー先の COleVariant の BSTR はデストラクターで後始末されますが、コピー元の VARIANT の BSTR はそのままです。
一方、VARIANT を VARIANT にコピーした場合は、単純なコピーになるので、ポインターをコピーするだけです。最後に VariantClear で後始末すれば、メモリーリークは起こりません。
余談ですが、#import を使用した場合は、GetValue が variant_t を返すので、これもメモリーリークは起こりません。
variant_t も COleVariant と同じようなクラスですが、個人的にはこっちの方が使いやすいです。
ご回答ありがとうございます。
なるほど、それが原因で解放されずにメモリリークを引き起こしていたのですね。
ようやく納得がいきました。
VARIANTもinitやClearを忘れるとえらいことになりますが、こちらの方がまだどこで使用しているかがわかりやすく判断しやすいと思いました。
頭を悩ませていたこの問題に一区切りできそうで、皆様のご協力、本当に感謝しております。
回答5件
あなたの回答
tips
プレビュー