एंटिटी फ्रेमवर्क async ऑपरेशन को पूरा होने में दस गुना समय लगता है


139

मुझे एक MVC साइट मिली है जो डेटाबेस को संभालने के लिए Entity Framework 6 का उपयोग कर रही है, और मैं इसे बदलने के लिए प्रयोग कर रहा हूं ताकि सब कुछ async नियंत्रकों के रूप में चलता रहे और डेटाबेस पर कॉल उनके async समकक्षों (जैसे। ToListAsync) को चलाया जाए। ToList () के बजाय

मुझे जो समस्या हो रही है, वह यह है कि बस अपने प्रश्नों को बदलकर async करने के लिए उन्हें अविश्वसनीय रूप से धीमा कर दिया है।

निम्न कोड को मेरे डेटा संदर्भ से "एल्बम" ऑब्जेक्ट का एक संग्रह मिलता है और इसका अनुवाद एक सरल डेटाबेस डेटाबेस में किया जाता है:

// Get the albums
var albums = await this.context.Albums
    .Where(x => x.Artist.ID == artist.ID)
    .ToListAsync();

यहाँ SQL जो बनाया गया है:

exec sp_executesql N'SELECT 
[Extent1].[ID] AS [ID], 
[Extent1].[URL] AS [URL], 
[Extent1].[ASIN] AS [ASIN], 
[Extent1].[Title] AS [Title], 
[Extent1].[ReleaseDate] AS [ReleaseDate], 
[Extent1].[AccurateDay] AS [AccurateDay], 
[Extent1].[AccurateMonth] AS [AccurateMonth], 
[Extent1].[Type] AS [Type], 
[Extent1].[Tracks] AS [Tracks], 
[Extent1].[MainCredits] AS [MainCredits], 
[Extent1].[SupportingCredits] AS [SupportingCredits], 
[Extent1].[Description] AS [Description], 
[Extent1].[Image] AS [Image], 
[Extent1].[HasImage] AS [HasImage], 
[Extent1].[Created] AS [Created], 
[Extent1].[Artist_ID] AS [Artist_ID]
FROM [dbo].[Albums] AS [Extent1]
WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134

जैसा कि चीजें चलती हैं, यह एक जटिल जटिल क्वेरी नहीं है, लेकिन इसे चलाने के लिए SQL सर्वर के लिए लगभग 6 सेकंड लग रहे हैं। SQL सर्वर प्रोफाइलर इसे पूरा करने के लिए 5742ms लेने के रूप में रिपोर्ट करता है।

अगर मैं अपना कोड बदलूं:

// Get the albums
var albums = this.context.Albums
    .Where(x => x.Artist.ID == artist.ID)
    .ToList();

फिर ठीक वही SQL उत्पन्न होता है, फिर भी SQL सर्वर प्रोफाइलर के अनुसार यह केवल 474ms में चलता है।

डेटाबेस में "एल्बम" तालिका में लगभग 3500 पंक्तियाँ हैं, जो वास्तव में बहुत अधिक नहीं है, और "कलाकार_आईडी" कॉलम पर एक सूचकांक है, इसलिए यह बहुत तेज़ होना चाहिए।

मुझे पता है कि async में ओवरहेड्स हैं, लेकिन चीजें दस गुना धीमी हो जाती हैं, मेरे लिए थोड़ा कठिन लगता है! मैं यहाँ गलत कहाँ जा रहा हूँ?


यह मुझे सही नहीं लगता। यदि आप समान डेटा के साथ एक ही क्वेरी को निष्पादित करते हैं, तो SQL सर्वर प्रोफाइलर द्वारा रिपोर्ट किया गया निष्पादन समय कम या ज्यादा होना चाहिए क्योंकि async क्या c # में होता है, Sql नहीं। Sql सर्वर को यह भी पता नहीं है कि आपका c # कोड async है
खान

जब आप पहली बार अपनी जेनरेट की गई क्वेरी को चलाते हैं, तो दूसरी बार से क्वेरी को कंपाइल करने में (एक्जीक्यूटिव प्लान, प्लान ...) बनाने में थोड़ा अधिक समय लग सकता है, वही क्वेरी तेज हो सकती है (Sql सर्वर क्वेरी को कैश करता है), लेकिन बहुत अलग नहीं होना चाहिए।
खान

3
आपको यह निर्धारित करने की आवश्यकता है कि धीमा क्या है। एक अनंत लूप में क्वेरी चलाएँ। डिबगर को 10 बार रोकें। यह सबसे अधिक बार कहां रुकता है? स्टैक को बाहरी कोड सहित पोस्ट करें।
usr

1
ऐसा लगता है कि समस्या छवि संपत्ति के साथ करने की है, जिसे मैं पूरी तरह से भूल गया हूं। यह एक VARBINARY (MAX) कॉलम है, इसलिए धीमापन पैदा करने के लिए बाध्य है, लेकिन यह अभी भी थोड़ा अजीब है कि धीमापन केवल एक मुद्दा है जो async चल रहा है। मैंने अपने डेटाबेस का पुनर्गठन किया है ताकि छवियां अब एक लिंक की गई तालिका का हिस्सा हों और अब सब कुछ बहुत तेज़ हो।
डायलन पैरी

1
समस्या यह हो सकती है कि ईएफ उन सभी बाइट्स और पंक्तियों को पुनः प्राप्त करने के लिए ADO.NET को async रीड्स के टन जारी कर रहा है। इस तरह ओवरहेड को बढ़ाया जाता है। चूंकि आपने माप नहीं किया था इसलिए मैंने पूछा कि हम कभी नहीं जान पाएंगे। समस्या हल होती दिख रही है।
यूएसआर

जवाबों:


286

मुझे यह सवाल बहुत दिलचस्प लगा, खासकर जब से मैं asyncAdo.Net और EF 6 के साथ हर जगह उपयोग कर रहा हूं। मैं उम्मीद कर रहा था कि कोई इस प्रश्न के लिए स्पष्टीकरण देगा, लेकिन ऐसा नहीं हुआ। इसलिए मैंने अपनी तरफ से इस समस्या को दोहराने की कोशिश की। मुझे उम्मीद है कि आप में से कुछ को यह दिलचस्प लगेगा।

पहली अच्छी खबर: मैंने इसे पुन: पेश किया :) और अंतर बहुत बड़ा है। एक कारक 8 के साथ ...

पहला परिणाम

सबसे पहले मैं के साथ काम कर कुछ शक था CommandBehaviorके बाद से, मैं एक दिलचस्प लेख को पढ़ने के बारे में asyncहलचल के साथ इस कहा:

"चूंकि गैर-अनुक्रमिक पहुंच मोड में संपूर्ण पंक्ति के लिए डेटा संग्रहीत करना पड़ता है, इसलिए यह समस्या पैदा कर सकता है यदि आप सर्वर से एक बड़ा कॉलम पढ़ रहे हैं (जैसे कि varbinary (MAX), varchar (MAX), nvarchar (MAX) या XML )। "

मुझे संदेह था कि ToList()कॉल होने के लिए CommandBehavior.SequentialAccessऔर async वाले होने के लिए CommandBehavior.Default(गैर-अनुक्रमिक है, जो मुद्दों का कारण बन सकता है)। इसलिए मैंने EF6 के स्रोतों को डाउनलोड किया, और हर जगह ब्रेकपॉइंट्स लगाए (जहां CommandBehaviorजहां इस्तेमाल किया, निश्चित रूप से)।

परिणाम: कुछ नहीं । सभी कॉल किए जाते हैं CommandBehavior.Default.... इसलिए मैंने यह समझने के लिए ईएफ कोड में कदम रखने की कोशिश की कि क्या होता है ... और .. ऊऊच ... मैंने कभी ऐसा प्रतिनिधि कोड नहीं देखा, सब कुछ आलसी लगता है ...

इसलिए मैंने यह समझने की कोशिश की कि क्या होता है?

और मुझे लगता है कि मेरे पास कुछ है ...

यहां तालिका I बेंचमार्क बनाने के लिए मॉडल है, इसके अंदर 3500 लाइनें और प्रत्येक में 256 Kb यादृच्छिक डेटा है varbinary(MAX)। (एफई 6.1 - CodeFirst - CodePlex ):

public class TestContext : DbContext
{
    public TestContext()
        : base(@"Server=(localdb)\\v11.0;Integrated Security=true;Initial Catalog=BENCH") // Local instance
    {
    }
    public DbSet<TestItem> Items { get; set; }
}

public class TestItem
{
    public int ID { get; set; }
    public string Name { get; set; }
    public byte[] BinaryData { get; set; }
}

और यहाँ एक कोड है जिसका उपयोग मैंने टेस्ट डेटा, और बेंचमार्क EF बनाने के लिए किया था।

using (TestContext db = new TestContext())
{
    if (!db.Items.Any())
    {
        foreach (int i in Enumerable.Range(0, 3500)) // Fill 3500 lines
        {
            byte[] dummyData = new byte[1 << 18];  // with 256 Kbyte
            new Random().NextBytes(dummyData);
            db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData });
        }
        await db.SaveChangesAsync();
    }
}

using (TestContext db = new TestContext())  // EF Warm Up
{
    var warmItUp = db.Items.FirstOrDefault();
    warmItUp = await db.Items.FirstOrDefaultAsync();
}

Stopwatch watch = new Stopwatch();
using (TestContext db = new TestContext())
{
    watch.Start();
    var testRegular = db.Items.ToList();
    watch.Stop();
    Console.WriteLine("non async : " + watch.ElapsedMilliseconds);
}

using (TestContext db = new TestContext())
{
    watch.Restart();
    var testAsync = await db.Items.ToListAsync();
    watch.Stop();
    Console.WriteLine("async : " + watch.ElapsedMilliseconds);
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
        while (await reader.ReadAsync())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReaderAsync SequentialAccess : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
        while (await reader.ReadAsync())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReaderAsync Default : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
        while (reader.Read())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReader SequentialAccess : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = cmd.ExecuteReader(CommandBehavior.Default);
        while (reader.Read())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReader Default : " + watch.ElapsedMilliseconds);
    }
}

नियमित ईएफ कॉल ( .ToList()) के लिए, प्रोफाइलिंग "सामान्य" लगती है और पढ़ने में आसान है:

ToList ट्रेस

यहां हमें स्टॉपवॉच के साथ हमारे पास 8.4 सेकंड मिलते हैं (प्रोफाइल धीमा हो जाता है। हमें कॉल पथ के साथ HitCount = 3500 भी मिलता है, जो परीक्षण में 3500 लाइनों के अनुरूप है। टीडीएस पार्सर की ओर से, चीजें बदतर होने लगती हैं क्योंकि हम TryReadByteArray()विधि पर 118 353 कॉल पढ़ते हैं , जो बफरिंग लूप थे। (प्रत्येक byte[]256kb के लिए औसत 33.8 कॉल )

के लिए asyncमामला है, यह वास्तव में वास्तव में अलग है .... सबसे पहले, .ToListAsync()कॉल ThreadPool पर निर्धारित है, और उसके बाद प्रतीक्षा कर रहे थे। यहां कुछ भी अद्भुत नहीं है। लेकिन, अब, asyncथ्रेडपोल पर यहाँ नरक है:

ToListAsync नरक

सबसे पहले, पहले मामले में हम पूर्ण कॉल पथ के साथ सिर्फ 3500 हिट काउंट कर रहे थे, यहां हमारे पास 118 371 हैं। इसके अलावा, आपको उन सभी सिंक्रोनाइज़ेशन कॉल की कल्पना करनी होगी, जिन्हें मैंने स्क्रीनशूट पर नहीं डाला था ...

दूसरा, पहले मामले में, हम TryReadByteArray()विधि के लिए "सिर्फ 118 353" कॉल कर रहे थे , यहाँ हमारे पास 2 050 210 कॉल हैं! यह 17 गुना अधिक है ... (बड़े 1Mb सरणी के साथ एक परीक्षण पर, यह 160 गुना अधिक है)

इसके अलावा हैं:

  • 120 000 Taskउदाहरण बनाए
  • 727 519 पर Interlockedकॉल करें
  • 290 569 Monitorकॉल
  • 98 283 ExecutionContextउदाहरण, 264 481 कैप्चर के साथ
  • 208 733 पर SpinLockकॉल करता है

मेरा अनुमान है कि बफ़रिंग एक एस्सेन्ट तरीके (और एक अच्छा नहीं) से बना है, जिसमें समानांतर टास्क टीडीएस से डेटा पढ़ने की कोशिश कर रहे हैं। बाइनरी डेटा को पार्स करने के लिए बहुत सारे टास्क बनाए जाते हैं।

प्रारंभिक निष्कर्ष के रूप में, हम कह सकते हैं कि Async महान है, EF6 महान है, लेकिन वर्तमान कार्यान्वयन में async का EF6 का उपयोग एक प्रमुख ओवरहेड जोड़ता है, प्रदर्शन पक्ष, थ्रेडिंग पक्ष, और CPU पक्ष (12% CPU उपयोग में) ToList()मामला और ToListAsync8 से 10 गुना लंबे समय तक काम करने के मामले में 20% ... मैं इसे एक पुराने i7 920 पर चलाता हूं)।

कुछ परीक्षण करते समय, मैं इस लेख के बारे में फिर से सोच रहा था और मुझे कुछ याद आ गया है:

".Net 4.5 में नए अतुल्यकालिक तरीकों के लिए, उनका व्यवहार बिल्कुल तुल्यकालिक विधियों के समान है, सिवाय एक उल्लेखनीय अपवाद के: गैर-अनुक्रमिक मोड में रीडअंश।"

क्या ?!!!

इसलिए मैं Ado.Net को नियमित / async कॉल और CommandBehavior.SequentialAccess/ / के साथ शामिल करने के लिए अपने बेंचमार्क का विस्तार करता हूं CommandBehavior.Default, और यहां एक बड़ा आश्चर्य है! :

ado के साथ

हमारे पास Ado.Net के साथ ठीक वैसा ही व्यवहार है !!! Facepalm ...

मेरा निश्चित निष्कर्ष है : EF 6 कार्यान्वयन में एक बग है। यह टॉगल चाहिए CommandBehaviorकरने के लिए SequentialAccessएक async कॉल एक से युक्त एक मेज पर किया जाता है जब binary(max)स्तंभ। प्रक्रिया को धीमा करते हुए बहुत सारे टास्क बनाने की समस्या Ado.Net की तरफ है। EF समस्या यह है कि यह Ado.Net का उपयोग नहीं करता है जैसा कि इसे करना चाहिए।

अब आप EF6 async विधियों का उपयोग करने के बजाय जानते हैं, तो आपको EF को नियमित रूप से गैर- async तरीके से कॉल करना होगा, और फिर एक का उपयोग करना होगा TaskCompletionSource<T> परिणाम को async तरीके से वापस करने के लिए होगा।

नोट 1: मैंने एक शर्मनाक त्रुटि के कारण अपनी पोस्ट को संपादित किया .... मैंने अपना पहला परीक्षण नेटवर्क पर किया है, स्थानीय रूप से नहीं, और सीमित बैंडविड्थ ने परिणामों को विकृत कर दिया है। यहाँ अद्यतन परिणाम हैं।

नोट 2: मैंने अन्य उपयोग मामलों के लिए अपने परीक्षण का विस्तार नहीं किया (उदा: nvarchar(max) बहुत अधिक डेटा के साथ) में नहीं किया है, लेकिन संभावना है कि समान व्यवहार होता है।

नोट 3: ToList()मामले के लिए कुछ सामान्य है, 12% सीपीयू (1/8 ऑफ माय सीपीयू = 1 लॉजिकल कोर) है। कुछ असामान्य ToListAsync()मामला के लिए अधिकतम 20% है , जैसे कि शेड्यूलर सभी Treads का उपयोग नहीं कर सकता है। यह शायद बहुत सारे टास्क के कारण, या शायद टीडीएस पार्सर में अड़चन है, मुझे नहीं पता ...


2
मैंने कोडप्लेक्स पर एक मुद्दा खोला, आशा है कि वे इसके बारे में कुछ करेंगे। Unitframework.codeplex.com/workitem/2686
rducom

3
मैंने github पर होस्ट किए गए नए EF कोड रेपो पर एक मुद्दा खोला: github.com/aspnet/EntityFramework6/issues/88
कोरेम

5
दुःख की बात है कि GitHub के मुद्दे को varbinary के साथ async का उपयोग नहीं करने की सलाह के साथ बंद कर दिया गया है। सिद्धांत रूप में वार्बिनरी वह मामला होना चाहिए जहां async सबसे अधिक समझ में आता है क्योंकि फ़ाइल संचारित होने के दौरान धागा लंबे समय तक अवरुद्ध रहेगा। तो अब हम डीबी में बाइनरी डेटा को बचाना चाहते हैं तो हम क्या करते हैं?
स्टिलगर

8
किसी को पता है कि क्या यह अभी भी ईएफ कोर में एक मुद्दा है? मुझे कोई जानकारी या बेंचमार्क नहीं मिला है।
एंड्रयू लुईस

2
@AndrewLewis मेरे पास इसके पीछे कोई विज्ञान नहीं है, लेकिन मैं EF कोर के साथ बार-बार कनेक्शन पूल टाइमआउट कर रहा हूं जहां दो प्रश्न पैदा कर रहे हैं .ToListAsync()और .CountAsync()... किसी और को यह टिप्पणी धागा ढूंढने में, यह क्वेरी मदद कर सकती है। गॉडस्पीडः।
स्कॉट

2

क्योंकि मुझे कुछ दिनों पहले इस सवाल का लिंक मिला, मैंने एक छोटा सा अपडेट पोस्ट करने का फैसला किया। मैं वर्तमान में, EF (6.4.0) और .NET फ्रेमवर्क 4.7.2 के नवीनतम संस्करण का उपयोग करके मूल उत्तर के परिणामों को पुन: पेश करने में सक्षम था । आश्चर्यजनक रूप से इस समस्या में कभी सुधार नहीं हुआ।

.NET Framework 4.7.2 | EF 6.4.0 (Values in ms. Average of 10 runs)

non async : 3016
async : 20415
ExecuteReaderAsync SequentialAccess : 2780
ExecuteReaderAsync Default : 21061
ExecuteReader SequentialAccess : 3467
ExecuteReader Default : 3074

इस सवाल का जवाब दिया: क्या डॉटनेट कोर में सुधार हुआ है?

मैंने मूल उत्तर से कोड को एक नए डॉटनेट कोर 3.1.3 प्रोजेक्ट में कॉपी किया और ईएफ कोर 3.1.3 जोड़ा। परिणाम हैं:

dotnet core 3.1.3 | EF Core 3.1.3 (Values in ms. Average of 10 runs)

non async : 2780
async : 6563
ExecuteReaderAsync SequentialAccess : 2593
ExecuteReaderAsync Default : 6679
ExecuteReader SequentialAccess : 2668
ExecuteReader Default : 2315

आश्चर्यजनक रूप से इसमें बहुत सुधार हुआ है। अभी भी कुछ समय लग रहा है क्योंकि थ्रेडपूल कहा जाता है, लेकिन यह .NET फ्रेमवर्क कार्यान्वयन से लगभग 3 गुना तेज है।

मुझे उम्मीद है कि यह जवाब अन्य लोगों को मदद करता है जो भविष्य में इस तरह से भेजते हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.