문제:
컬렉션에 저장하고 나중에 해당 컬렉션에 대해 검색을 수행하려는 약 120,000 명의 사용자 (문자열) 의 텍스트 파일 이 있습니다.
검색 방법은 사용자가 a의 텍스트를 변경할 때마다 발생 TextBox
하며 결과는 의 텍스트 를 포함 하는 문자열이어야합니다 TextBox
.
목록을 변경할 필요가 없습니다. 결과를 가져 와서 ListBox
.
지금까지 시도한 것 :
두 개의 다른 컬렉션 / 컨테이너로 시도했는데, 외부 텍스트 파일에서 문자열 항목을 덤프합니다 (물론 한 번).
List<string> allUsers;
HashSet<string> allUsers;
다음 LINQ 쿼리를 사용합니다 .
allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();
내 검색 이벤트 (사용자가 검색 텍스트를 변경하면 실행 됨) :
private void textBox_search_TextChanged(object sender, EventArgs e)
{
if (textBox_search.Text.Length > 2)
{
listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();
}
else
{
listBox_choices.DataSource = null;
}
}
결과 :
둘 다 저에게 응답 시간이 좋지 않았습니다 (각 키 누름 사이에 약 1-3 초).
질문:
내 병목이 어디에 있다고 생각하세요? 내가 사용한 컬렉션? 검색 방법? 양자 모두?
더 나은 성능과 더 유창한 기능을 얻으려면 어떻게해야합니까?
답변
완료되면 콜백 메서드를 호출하는 백그라운드 스레드에서 필터링 작업을 수행하거나 입력이 변경된 경우 필터링을 다시 시작할 수 있습니다.
일반적인 아이디어는 다음과 같이 사용할 수 있다는 것입니다.
public partial class YourForm : Form
{
private readonly BackgroundWordFilter _filter;
public YourForm()
{
InitializeComponent();
// setup the background worker to return no more than 10 items,
// and to set ListBox.DataSource when results are ready
_filter = new BackgroundWordFilter
(
items: GetDictionaryItems(),
maxItemsToMatch: 10,
callback: results =>
this.Invoke(new Action(() => listBox_choices.DataSource = results))
);
}
private void textBox_search_TextChanged(object sender, EventArgs e)
{
// this will update the background worker's "current entry"
_filter.SetCurrentEntry(textBox_search.Text);
}
}
대략적인 스케치는 다음과 같습니다.
public class BackgroundWordFilter : IDisposable
{
private readonly List<string> _items;
private readonly AutoResetEvent _signal = new AutoResetEvent(false);
private readonly Thread _workerThread;
private readonly int _maxItemsToMatch;
private readonly Action<List<string>> _callback;
private volatile bool _shouldRun = true;
private volatile string _currentEntry = null;
public BackgroundWordFilter(
List<string> items,
int maxItemsToMatch,
Action<List<string>> callback)
{
_items = items;
_callback = callback;
_maxItemsToMatch = maxItemsToMatch;
// start the long-lived backgroud thread
_workerThread = new Thread(WorkerLoop)
{
IsBackground = true,
Priority = ThreadPriority.BelowNormal
};
_workerThread.Start();
}
public void SetCurrentEntry(string currentEntry)
{
// set the current entry and signal the worker thread
_currentEntry = currentEntry;
_signal.Set();
}
void WorkerLoop()
{
while (_shouldRun)
{
// wait here until there is a new entry
_signal.WaitOne();
if (!_shouldRun)
return;
var entry = _currentEntry;
var results = new List<string>();
// if there is nothing to process,
// return an empty list
if (string.IsNullOrEmpty(entry))
{
_callback(results);
continue;
}
// do the search in a for-loop to
// allow early termination when current entry
// is changed on a different thread
foreach (var i in _items)
{
// if matched, add to the list of results
if (i.Contains(entry))
results.Add(i);
// check if the current entry was updated in the meantime,
// or we found enough items
if (entry != _currentEntry || results.Count >= _maxItemsToMatch)
break;
}
if (entry == _currentEntry)
_callback(results);
}
}
public void Dispose()
{
// we are using AutoResetEvent and a background thread
// and therefore must dispose it explicitly
Dispose(true);
}
private void Dispose(bool disposing)
{
if (!disposing)
return;
// shutdown the thread
if (_workerThread.IsAlive)
{
_shouldRun = false;
_currentEntry = null;
_signal.Set();
_workerThread.Join();
}
// if targetting .NET 3.5 or older, we have to
// use the explicit IDisposable implementation
(_signal as IDisposable).Dispose();
}
}
또한 _filter
부모 Form
가 삭제 될 때 실제로 인스턴스 를 삭제해야합니다 . 즉, Form
의 Dispose
메서드 ( YourForm.Designer.cs
파일 내부 )를 열고 다음과 같이 편집해야합니다 .
// inside "xxxxxx.Designer.cs"
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_filter != null)
_filter.Dispose();
// this part is added by Visual Studio designer
if (components != null)
components.Dispose();
}
base.Dispose(disposing);
}
내 컴퓨터에서는 매우 빠르게 작동하므로 더 복잡한 솔루션을 찾기 전에이를 테스트하고 프로파일 링해야합니다.
즉, “더 복잡한 솔루션”은 마지막 두 개의 결과를 사전에 저장 한 다음 새 항목이 마지막 문자의 첫 번째 문자 만 다른 것으로 밝혀진 경우에만 필터링하는 것입니다.
답변
몇 가지 테스트를 수행했으며 120,000 개의 항목 목록을 검색하고 항목으로 새 목록을 채우는 데는 무시할 수있는 시간이 걸립니다 (모든 문자열이 일치하더라도 약 1/50 초).
따라서 현재보고있는 문제는 데이터 소스 채우기에서 발생해야합니다.
listBox_choices.DataSource = ...
목록 상자에 너무 많은 항목을 넣는 것 같습니다.
다음과 같이 처음 20 개 항목으로 제한해야합니다.
listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text))
.Take(20).ToList();
또한 다른 사람들이 지적했듯이의 TextBox.Text
각 항목에 대한 속성에 액세스하고 있음을 유의하십시오 allUsers
. 다음과 같이 쉽게 수정할 수 있습니다.
string target = textBox_search.Text;
listBox_choices.DataSource = allUsers.Where(item => item.Contains(target))
.Take(20).ToList();
그러나 나는 TextBox.Text
50 만 번 액세스하는 데 걸리는 시간을 측정 했으며 OP에 언급 된 1-3 초보다 훨씬 적은 0.7 초 밖에 걸리지 않았습니다. 그래도 이것은 가치있는 최적화입니다.
답변
접미사 트리 사용 인덱스로. 또는 모든 이름의 모든 접미사를 해당 이름 목록과 연결하는 정렬 된 사전을 구축하십시오.
입력 :
Abraham
Barbara
Abram
구조는 다음과 같습니다.
a -> Barbara
ab -> Abram
abraham -> Abraham
abram -> Abram
am -> Abraham, Abram
aham -> Abraham
ara -> Barbara
arbara -> Barbara
bara -> Barbara
barbara -> Barbara
bram -> Abram
braham -> Abraham
ham -> Abraham
m -> Abraham, Abram
raham -> Abraham
ram -> Abram
rbara -> Barbara
검색 알고리즘
사용자 입력 “bra”를 가정합니다.
- 사용자 입력에 대해 사전을 양분 하여 사용자 입력 또는 이동할 수있는 위치를 찾습니다. 이렇게하면 “barbara”- “bra”보다 낮은 마지막 키를 찾습니다. “브라”의 하한이라고합니다. 검색에는 로그 시간이 걸립니다.
- 사용자 입력이 더 이상 일치하지 않을 때까지 찾은 키부터 계속 반복합니다. 이것은 “bram”-> Abram 및 “braham”-> Abraham을 줄 것입니다.
- 반복 결과 (Abram, Abraham)를 연결하고 출력합니다.
이러한 트리는 하위 문자열을 빠르게 검색하도록 설계되었습니다. 성능은 O (log n)에 가깝습니다. 이 접근 방식은 GUI 스레드에서 직접 사용할 수있을만큼 빠르게 작동 할 것이라고 생각합니다. 또한 동기화 오버 헤드가 없기 때문에 스레드 솔루션보다 빠르게 작동합니다.
답변
텍스트 검색 엔진 (예 : Lucene.Net ) 또는 데이터베이스 (예 : SQL CE , SQLite 등 의 임베디드 엔진을 고려할 수 있음 )가 필요합니다. 즉, 색인화 된 검색이 필요합니다. 해시 기반 검색은 하위 문자열을 검색하기 때문에 여기서 적용 할 수 없지만 해시 기반 검색은 정확한 값을 검색하는 데 적합합니다.
그렇지 않으면 컬렉션을 반복하는 반복 검색이됩니다.
답변
“디 바운스”유형의 이벤트를 갖는 것도 유용 할 수 있습니다. 이벤트를 시작하기 전에 변경이 완료 될 때까지 일정 시간 (예 : 200ms)을 기다린다는 점에서 스로틀 링과 다릅니다.
참조 디 바운스 및 스로틀 : 시각적 설명 디 바운싱에 대한 자세한 정보를 얻을 수 있습니다. 이 기사는 C # 대신 JavaScript에 초점을 맞추고 있지만 원칙이 적용됩니다.
이것의 장점은 여전히 쿼리를 입력 할 때 검색하지 않는다는 것입니다. 그런 다음 한 번에 두 개의 검색을 수행하려는 시도를 중지해야합니다.
답변
다른 스레드에서 검색을 실행하고 해당 스레드가 실행되는 동안 로딩 애니메이션이나 진행률 표시 줄을 표시합니다.
LINQ 쿼리 를 병렬화 할 수도 있습니다.
var queryResults = strings.AsParallel().Where(item => item.Contains("1")).ToList();
다음은 AsParallel ()의 성능 이점을 보여주는 벤치 마크입니다.
{
IEnumerable<string> queryResults;
bool useParallel = true;
var strings = new List<string>();
for (int i = 0; i < 2500000; i++)
strings.Add(i.ToString());
var stp = new Stopwatch();
stp.Start();
if (useParallel)
queryResults = strings.AsParallel().Where(item => item.Contains("1")).ToList();
else
queryResults = strings.Where(item => item.Contains("1")).ToList();
stp.Stop();
Console.WriteLine("useParallel: {0}\r\nTime Elapsed: {1}", useParallel, stp.ElapsedMilliseconds);
}
답변
최신 정보:
프로파일 링을했습니다.
(업데이트 3)
- 목록 내용 : 0에서 2.499.999까지 생성 된 숫자
- 필터 텍스트 : 123 (결과 20.477 개)
- Core i5-2500, Win7 64 비트, 8GB RAM
- VS2012 + JetBrains dotTrace
2.500.000 레코드에 대한 초기 테스트 실행에는 20.000ms가 소요되었습니다.
가장 큰 범인은 textBox_search.Text
내부에 대한 호출 Contains
입니다. 이렇게하면 get_WindowText
텍스트 상자 의 값 비싼 메서드에 대한 각 요소가 호출됩니다 . 코드를 다음과 같이 변경하기 만하면됩니다.
var text = textBox_search.Text;
listBox_choices.DataSource = allUsers.Where(item => item.Contains(text)).ToList();
실행 시간을 1.858ms 로 줄였습니다 .
업데이트 2 :
다른 두 가지 중요한 병목 현상은 이제 호출 string.Contains
(실행 시간의 약 45 %)과 목록 상자 요소의 업데이트 set_Datasource
(30 %)입니다.
Basilevs가 필요한 비교 횟수를 줄이고 키를 누른 후 검색에서 처리 시간을 파일에서 이름을로드 할 때까지 푸시하도록 제안했듯이 Suffix 트리를 생성하여 속도와 메모리 사용량 사이의 균형을 맞출 수 있습니다. 사용자에게 바람직 할 수 있습니다.
목록 상자에 요소를로드하는 성능을 높이려면 처음 몇 개의 요소 만로드하고 사용 가능한 추가 요소가 있음을 사용자에게 표시하는 것이 좋습니다. 이렇게하면 사용자에게 사용 가능한 결과가 있다는 피드백을 제공하여 더 많은 문자를 입력하여 검색을 구체화하거나 버튼을 눌러 전체 목록을로드 할 수 있습니다.
를 사용 BeginUpdate
하고 EndUpdate
의 실행 시간을 변경하지 않았습니다 set_Datasource
.
다른 사람들이 여기에서 언급했듯이 LINQ 쿼리 자체는 매우 빠르게 실행됩니다. 병목 현상이 목록 상자 자체를 업데이트하는 것이라고 생각합니다. 다음과 같이 시도해 볼 수 있습니다.
if (textBox_search.Text.Length > 2)
{
listBox_choices.BeginUpdate();
listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();
listBox_choices.EndUpdate();
}
이게 도움이 되길 바란다.