Seberapa mahal pernyataan kuncinya?

111

Saya telah bereksperimen dengan multi threading dan pemrosesan paralel dan saya membutuhkan penghitung untuk melakukan beberapa penghitungan dasar dan analisis statistik dari kecepatan pemrosesan. Untuk menghindari masalah dengan penggunaan kelas saya secara bersamaan, saya telah menggunakan pernyataan kunci pada variabel pribadi di kelas saya:

private object mutex = new object();

public void Count(int amount)
{
 lock(mutex)
 {
  done += amount;
 }
}

Tapi saya bertanya-tanya ... seberapa mahal mengunci variabel? Apa dampak negatifnya terhadap kinerja?

Kees C. Bakker
sumber
10
Mengunci variabel tidaklah mahal; itu adalah menunggu variabel terkunci yang ingin Anda hindari.
Gabe
53
itu jauh lebih murah daripada menghabiskan berjam-jam untuk melacak kondisi balapan lain ;-)
BrokenGlass
2
Nah ... jika kunci mahal Anda mungkin ingin menghindarinya dengan mengubah pemrograman sehingga membutuhkan lebih sedikit kunci. Saya bisa menerapkan semacam sinkronisasi.
Kees C. Bakker
1
Saya mengalami peningkatan dramatis dalam kinerja (sekarang, setelah membaca komentar @Gabe) hanya dengan memindahkan banyak kode dari blok kunci saya. Intinya: mulai sekarang saya hanya akan meninggalkan akses variabel (biasanya satu baris) di dalam blok kunci, semacam "penguncian tepat waktu". Apakah masuk akal?
heltonbiker
2
@ Heltonbiker Tentu saja masuk akal. Itu juga harus prinsip arsitektural, Anda harus membuat kunci sesingkat, sesederhana dan secepat mungkin. Hanya data yang benar-benar diperlukan yang perlu disinkronkan. Pada kotak server, Anda juga harus mempertimbangkan sifat hibrid dari kunci tersebut. Pertentangan bahkan jika tidak penting untuk kode Anda adalah berkat sifat hibrid dari kunci yang menyebabkan inti berputar selama setiap akses jika kunci dipegang oleh orang lain. Anda secara efektif melahap beberapa sumber daya cpu dari layanan lain di server untuk beberapa waktu sebelum utas Anda ditangguhkan.
ipavlu

Jawaban:

86

Berikut adalah artikel yang membahas biayanya. Jawaban singkatnya adalah 50ns.

Jake Pearson
sumber
39
Jawaban singkat yang lebih baik: 50ns + waktu yang dihabiskan untuk menunggu jika utas lain menahan kunci.
Herman
4
Semakin banyak utas yang masuk dan keluar dari kunci, semakin mahal harganya. Biaya meningkat secara eksponensial dengan jumlah utas
Arsen Zahray
16
Beberapa konteks: membagi dua angka pada 3Ghz x86 membutuhkan waktu sekitar 10ns (tidak termasuk waktu yang dibutuhkan untuk mengambil / mendekode instruksi) ; dan memuat satu variabel dari memori (non-cache) ke dalam register membutuhkan waktu sekitar 40ns. Jadi 50ns adalah sangat cepat , sangat cepat - Anda tidak perlu khawatir tentang biaya penggunaan locklebih dari yang Anda khawatirkan tentang biaya menggunakan variabel.
BlueRaja - Danny Pflughoeft
3
Juga, artikel itu sudah tua ketika pertanyaan ini diajukan.
Otis
3
Metrik yang sangat bagus, "hampir tanpa biaya", belum lagi salah. Kalian jangan di pertimbangkan, singkat dan cepat saja dan HANYA jika tidak ada perselisihan sama sekali, satu thread. DALAM KASUS TERSEBUT, Anda TIDAK PERLU KUNCI SAMA SEKALI. Masalah kedua, kunci bukan kunci, tetapi kunci hibrid, mendeteksi di dalam CLR bahwa kunci tidak dipegang oleh siapa pun berdasarkan operasi atom dan dalam kasus seperti itu, ia menghindari panggilan ke inti sistem operasi, yaitu cincin berbeda yang tidak diukur oleh ini tes. Apa yang diukur sebagai 25ns hingga 50ns sebenarnya adalah kode instruksi tingkat aplikasi yang saling bertautan jika kunci tidak diambil
ipavlu
50

Jawaban teknisnya adalah bahwa ini tidak mungkin untuk dihitung, hal ini sangat bergantung pada status buffer tulis kembali memori CPU dan berapa banyak data yang dikumpulkan prefetcher harus dibuang dan dibaca ulang. Keduanya sangat non-deterministik. Saya menggunakan 150 siklus CPU sebagai perkiraan belakang amplop yang menghindari kekecewaan besar.

Jawaban praktis adalah bahwa hal itu waaaay lebih murah daripada jumlah waktu Anda akan membakar pada debugging kode Anda ketika Anda berpikir Anda dapat melewatkan kunci.

Untuk mendapatkan angka pasti, Anda harus mengukur. Visual Studio memiliki penganalisis konkurensi apik yang tersedia sebagai ekstensi.

Hans Passant
sumber
1
Sebenarnya tidak, itu bisa diukur dan diukur. Ini tidak semudah menulis kunci itu di sekitar kode, lalu menyatakan bahwa semuanya hanya 50ns, sebuah mitos yang diukur pada akses utas tunggal ke kunci.
ipavlu
8
"Saya pikir Anda bisa melewatkan kunci" ... Saya pikir di situlah banyak orang berada ketika mereka membaca pertanyaan ini ...
Snoop
30

Bacaan lebih lanjut:

Saya ingin menyajikan beberapa artikel saya, yang tertarik pada sinkronisasi umum primitif dan mereka menggali Monitor, perilaku pernyataan kunci C #, properti, dan biaya tergantung pada skenario yang berbeda dan jumlah utas. Ini secara khusus tertarik tentang pemborosan CPU dan periode throughput untuk memahami berapa banyak pekerjaan yang dapat didorong dalam berbagai skenario:

https://www.codeproject.com/Articles/1236238/Unified-Concurrency-I-Introduction https://www.codeproject.com/Articles/1237518/Unified-Concurrency-II-benchmarking-methodologies https: // www. codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking

Jawaban asli:

Oh sayang!

Tampaknya jawaban yang benar yang ditandai di sini sebagai JAWABAN pada dasarnya salah! Saya ingin meminta penulis jawabannya, dengan hormat, untuk membaca artikel yang ditautkan sampai akhir. artikel

Penulis artikel dari tahun 2003 pasal itu mengukur pada mesin Dual Core saja dan dalam kasus pengukuran pertama, ia diukur mengunci dengan thread tunggal hanya dan hasilnya adalah sekitar 50ns per akses kunci.

Ia tidak mengatakan apa-apa tentang kunci di lingkungan bersamaan. Jadi kita harus melanjutkan membaca artikel dan di paruh kedua, penulis mengukur skenario penguncian dengan dua dan tiga utas, yang mendekati tingkat konkurensi prosesor saat ini.

Jadi penulis mengatakan, bahwa dengan dua utas pada Dual Core, kuncinya berharga 120ns, dan dengan 3 utas harganya menjadi 180ns. Jadi tampaknya jelas bergantung pada jumlah utas yang mengakses kunci secara bersamaan.

Jadi sederhana, bukan 50 ns kecuali jika itu adalah utas tunggal, di mana kuncinya menjadi tidak berguna.

Masalah lain yang perlu dipertimbangkan adalah bahwa waktu tersebut diukur sebagai waktu rata - rata !

Jika waktu iterasi akan diukur, akan ada waktu antara 1ms hingga 20ms, hanya karena mayoritas cepat, tetapi beberapa utas akan menunggu waktu prosesor dan bahkan menimbulkan penundaan milidetik.

Ini adalah berita buruk untuk semua jenis aplikasi yang membutuhkan throughput tinggi, latensi rendah.

Dan masalah terakhir yang perlu dipertimbangkan adalah bahwa mungkin ada operasi yang lebih lambat di dalam kunci dan seringkali demikian. Semakin lama blok kode dieksekusi di dalam kunci, semakin tinggi pertikaian dan penundaan meningkat setinggi langit.

Harap pertimbangkan, bahwa lebih dari satu dekade telah berlalu dari tahun 2003, itu adalah beberapa generasi prosesor yang dirancang khusus untuk berjalan secara penuh secara bersamaan dan penguncian sangat merugikan kinerja mereka.

ipavlu
sumber
1
Untuk memperjelas, artikel tersebut tidak mengatakan kinerja kunci menurun dengan jumlah utas dalam aplikasi; kinerja menurun dengan jumlah utas bersaing di atas kunci. (Itu tersirat, tetapi tidak dinyatakan dengan jelas, dalam jawaban di atas.)
Gooseberry
Saya kira yang Anda maksud adalah ini: "Jadi tampaknya jelas bergantung pada jumlah utas yang diakses secara bersamaan dan lebih banyak lebih buruk." Ya, kata-katanya bisa lebih baik. Maksud saya "diakses secara bersamaan" sebagai utas yang mengakses kunci secara bersamaan, sehingga menimbulkan perselisihan.
ipavlu
20

Ini tidak menjawab pertanyaan Anda tentang kinerja, tetapi saya dapat mengatakan bahwa .NET Framework memang menawarkan Interlocked.Addmetode yang memungkinkan Anda menambahkan Anda amountke doneanggota Anda tanpa mengunci objek lain secara manual.

Adam Maras
sumber
1
Ya, ini mungkin jawaban terbaik. Tetapi terutama karena alasan kode yang lebih pendek dan lebih bersih. Perbedaan kecepatan sepertinya tidak akan terlihat.
Henk Holterman
terima kasih atas jawaban ini. Saya melakukan lebih banyak hal dengan kunci. Int yang ditambahkan adalah salah satu dari banyak. Suka saran itu, akan menggunakannya mulai sekarang.
Kees C. Bakker
kunci jauh, jauh lebih mudah dilakukan dengan benar, meskipun kode bebas kunci berpotensi lebih cepat. Interlocked.Add sendiri memiliki masalah yang sama dengan + = tanpa sinkronisasi.
hanggar
10

lock (Monitor.Enter / Exit) sangat murah, lebih murah daripada alternatif seperti Waithandle atau Mutex.

Tetapi bagaimana jika (sedikit) lambat, apakah Anda lebih suka program yang cepat dengan hasil yang salah?

Henk Holterman
sumber
5
Haha ... Saya mengikuti program yang cepat dan hasilnya bagus.
Kees C. Bakker
@ henk-holterman Ada beberapa masalah dengan pernyataan Anda: Pertama seperti yang ditunjukkan dengan jelas oleh pertanyaan dan jawaban ini, pemahaman yang rendah tentang dampak penguncian pada keseluruhan kinerja, bahkan orang yang menyatakan mitos tentang 50ns yang hanya dapat diterapkan dengan lingkungan utas tunggal. Kedua, pernyataan Anda ada di sini dan akan bertahan selama bertahun-tahun dan sementara itu, prosesor tumbuh dalam inti, tetapi kecepatan inti tidak terlalu besar. ** Ketiga ** aplikasi menjadi semakin kompleks seiring waktu, dan kemudian berlapis-lapis mengunci di lingkungan banyak inti dan jumlahnya meningkat, 2,4,8,10,20,16,32
ipavlu
Pendekatan saya yang biasa adalah membangun sinkronisasi dengan cara yang digabungkan secara longgar dengan interaksi sesedikit mungkin. Itu berjalan sangat cepat untuk struktur data tanpa kunci. Saya membuat pembungkus kode saya di sekitar spinlock untuk menyederhanakan pengembangan dan bahkan ketika TPL memiliki koleksi konkuren khusus, saya telah mengembangkan koleksi spin-lock saya sendiri di sekitar daftar, larik, kamus dan antrian, karena saya membutuhkan sedikit lebih banyak kendali dan terkadang beberapa kode berjalan di bawah spinlock. Saya dapat memberitahu Anda, itu mungkin dan memungkinkan untuk menyelesaikan beberapa skenario yang tidak dapat dilakukan oleh koleksi TPL dan dengan kinerja / perolehan throughput yang hebat.
ipavlu
7

Biaya untuk mengunci dalam loop yang rapat, dibandingkan dengan alternatif tanpa kunci, sangat besar. Anda dapat melakukan loop berkali-kali dan masih lebih efisien daripada kunci. Itulah mengapa antrian bebas kunci sangat efisien.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LockPerformanceConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            const int LoopCount = (int) (100 * 1e6);
            int counter = 0;

            for (int repetition = 0; repetition < 5; repetition++)
            {
                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    lock (stopwatch)
                        counter = i;
                stopwatch.Stop();
                Console.WriteLine("With lock: {0}", stopwatch.ElapsedMilliseconds);

                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    counter = i;
                stopwatch.Stop();
                Console.WriteLine("Without lock: {0}", stopwatch.ElapsedMilliseconds);
            }

            Console.ReadKey();
        }
    }
}

Keluaran:

With lock: 2013
Without lock: 211
With lock: 2002
Without lock: 210
With lock: 1989
Without lock: 210
With lock: 1987
Without lock: 207
With lock: 1988
Without lock: 208
Johan Nilsson
sumber
4
Ini mungkin contoh yang buruk karena loop Anda benar-benar tidak melakukan apa-apa, selain dari satu tugas variabel dan kunci setidaknya 2 panggilan fungsi. Juga, 20ns per kunci yang Anda dapatkan tidak terlalu buruk.
Zar Shardan
5

Ada beberapa cara berbeda untuk mendefinisikan "biaya". Ada biaya overhead yang sebenarnya untuk mendapatkan dan melepaskan kunci; seperti yang ditulis Jake, hal itu dapat diabaikan kecuali operasi ini dilakukan jutaan kali.

Yang lebih relevan adalah efeknya pada aliran eksekusi. Kode ini hanya dapat dimasukkan oleh satu utas dalam satu waktu. Jika Anda memiliki 5 utas yang melakukan operasi ini secara teratur, 4 di antaranya akan menunggu kunci dilepaskan, dan kemudian menjadi utas pertama yang dijadwalkan untuk memasukkan potongan kode itu setelah kunci itu dilepaskan. Jadi, algoritme Anda akan sangat terpengaruh. Seberapa banyak tergantung pada algoritme dan seberapa sering operasi dipanggil .. Anda tidak dapat benar-benar menghindarinya tanpa memperkenalkan kondisi balapan, tetapi Anda dapat memperbaikinya dengan meminimalkan jumlah panggilan ke kode yang terkunci.

KeithS
sumber